From dfaafec6599104be5df70e4afb3cc3314ff070fe Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 12:08:07 +0100 Subject: [PATCH 01/93] Accept legacy and normalized constant-buffer offset fields --- docs/pr-metadata-offsets.md | 5 +++ libs/metadata.py | 18 +++++++-- tests/test_metadata_offsets.py | 73 ++++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 docs/pr-metadata-offsets.md create mode 100644 tests/test_metadata_offsets.py diff --git a/docs/pr-metadata-offsets.md b/docs/pr-metadata-offsets.md new file mode 100644 index 0000000..1692765 --- /dev/null +++ b/docs/pr-metadata-offsets.md @@ -0,0 +1,5 @@ +# Accept both serialized constant-buffer offset names + +Read `m_OffsetInConstantBuffer` when present and the legacy `m_Index` otherwise. AssetRipper's `Pass002_RenameSubnodes` explicitly normalizes that rename for vector, matrix and struct parameters. Both layout readers now reject absent, negative, unaligned or non-integer offsets instead of inventing zero. + +Validation: five Python regression tests and all four existing PicaVoxel extraction/decompilation tests pass. All 51 baseline C# tests pass with an en-US default culture. Under the machine's fr-FR default, the unchanged golden test fails on decimal separators; that pre-existing defect is addressed separately by the numeric-formatting PR. No existing assertions or golden files are changed here. diff --git a/libs/metadata.py b/libs/metadata.py index 0861c10..82556c7 100644 --- a/libs/metadata.py +++ b/libs/metadata.py @@ -39,6 +39,16 @@ def save(self, out_path): # -- helpers -- + @staticmethod + def _parameter_offset(parameter): + # Unity renamed this field; AssetRipper also normalizes older exports. + field = "m_OffsetInConstantBuffer" if "m_OffsetInConstantBuffer" in parameter else "m_Index" + offset = parameter.get(field) + if type(offset) is not int or offset < 0 or offset % 4: + raise ValueError( + f"Invalid or missing constant-buffer byte offset for parameter {parameter.get('m_NameIndex')}: {offset!r}") + return offset + @staticmethod def _get(d, key, default=None): return d.get(key, default) if isinstance(d, dict) else default @@ -107,7 +117,7 @@ def _name_layouts(raw_passes): for v in b.get("m_VectorParams", []) or []: entry = { "name": index_to_name.get(v.get("m_NameIndex"), ""), - "offset": v.get("m_Index", 0), + "offset": ShaderMetadata._parameter_offset(v), "dim": v.get("m_Dim", 0), "arraySize": v.get("m_ArraySize", 0), } @@ -117,7 +127,7 @@ def _name_layouts(raw_passes): for m in b.get("m_MatrixParams", []) or []: entry = { "name": index_to_name.get(m.get("m_NameIndex"), ""), - "offset": m.get("m_Index", 0), + "offset": ShaderMetadata._parameter_offset(m), "rowCount": m.get("m_RowCount", 0), "arraySize": m.get("m_ArraySize", 0), "isMatrix": True, @@ -211,14 +221,14 @@ def _next_slot(): for v in b.get("m_VectorParams", []) or []: cb["variables"].append({ "name": index_to_name.get(v.get("m_NameIndex"), ""), - "offset": v.get("m_Index", 0), + "offset": ShaderMetadata._parameter_offset(v), "dim": v.get("m_Dim", 0), "arraySize": v.get("m_ArraySize", 0), }) for m in b.get("m_MatrixParams", []) or []: cb["variables"].append({ "name": index_to_name.get(m.get("m_NameIndex"), ""), - "offset": m.get("m_Index", 0), + "offset": ShaderMetadata._parameter_offset(m), "rowCount": m.get("m_RowCount", 0), "arraySize": m.get("m_ArraySize", 0), "isMatrix": True, diff --git a/tests/test_metadata_offsets.py b/tests/test_metadata_offsets.py new file mode 100644 index 0000000..1ec26ed --- /dev/null +++ b/tests/test_metadata_offsets.py @@ -0,0 +1,73 @@ +import json +import tempfile +import unittest +from pathlib import Path + +from libs.metadata import ShaderMetadata + + +class MetadataOffsetTests(unittest.TestCase): + def extract(self, vector, matrix): + shader_pass = { + "m_NameIndices": {"Globals": 1, "Tint": 2, "Transform": 3}, + "m_ProgVertex": {"m_CommonParameters": { + "m_ConstantBufferBindings": [{"m_NameIndex": 1, "m_Index": 2}], + "m_ConstantBuffers": [{ + "m_NameIndex": 1, "m_Size": 128, + "m_VectorParams": [{"m_NameIndex": 2, "m_Dim": 3, **vector}], + "m_MatrixParams": [{"m_NameIndex": 3, "m_RowCount": 4, **matrix}], + }], + }}, + } + shader = {"m_Name": "Test/Offsets", "m_ParsedForm": { + "m_SubShaders": [{"m_Passes": [shader_pass]}], + }} + with tempfile.TemporaryDirectory() as folder: + path = Path(folder) / "shader.json" + path.write_text(json.dumps(shader), encoding="utf-8") + extractor = ShaderMetadata(path) + # Exercise both readers: the shared layout and the stage-local layout. + shared = extractor._name_layouts([shader_pass])["Globals"] + local = extractor._constant_buffers( + shader_pass, ["m_ProgVertex"], {1: "Globals", 2: "Tint", 3: "Transform"}) + built = extractor.build()["subShaders"][0]["passes"][0]["constantBuffers"] + self.assertEqual(local, built) + self.assertEqual(local[0]["slot"], 2) + self.assertEqual(shared, local[0]["variables"]) + return [(entry["name"], entry["offset"]) for entry in shared] + + def test_both_serialized_names_preserve_byte_offsets(self): + for field in ("m_Index", "m_OffsetInConstantBuffer"): + with self.subTest(field=field): + self.assertEqual(self.extract({field: 20}, {field: 48}), + [("Tint", 20), ("Transform", 48)]) + + def test_explicit_offset_takes_precedence(self): + self.assertEqual(self.extract( + {"m_Index": 0, "m_OffsetInConstantBuffer": 20}, + {"m_Index": 0, "m_OffsetInConstantBuffer": 48}), + [("Tint", 20), ("Transform", 48)]) + + def test_zero_is_a_valid_offset(self): + self.assertEqual(self.extract({"m_Index": 0}, {"m_OffsetInConstantBuffer": 0}), + [("Tint", 0), ("Transform", 0)]) + + def test_missing_offsets_fail_in_both_readers(self): + for vector, matrix in (({}, {"m_Index": 48}), ({"m_Index": 20}, {})): + with self.subTest(vector=vector, matrix=matrix): + with self.assertRaisesRegex(ValueError, "byte offset"): + self.extract(vector, matrix) + + def test_invalid_offsets_are_not_replaced_by_legacy_values(self): + for value in (None, True, -4, 2, 1.5, "16"): + for field in ("m_Index", "m_OffsetInConstantBuffer"): + with self.subTest(value=value, field=field): + invalid = {"m_Index": 20, field: value} + with self.assertRaisesRegex(ValueError, "byte offset"): + self.extract(invalid, {"m_Index": 48}) + with self.assertRaisesRegex(ValueError, "byte offset"): + self.extract({"m_Index": 20}, invalid) + + +if __name__ == "__main__": + unittest.main() From 10077c49bfc6e536091df0e401499802277aa5b7 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 12:13:13 +0100 Subject: [PATCH 02/93] Read SFI0 as a 64-bit feature mask --- Shader Decompiler/DXBC/Chunks/SFI0 Chunk.cs | 81 ++----------------- Shader Decompiler/Tests/SpecTestVectors.cs | 24 ++---- docs/pr-dxbc-feature-flags.md | 7 ++ .../BlobAndDxbcTests.cs | 16 ++-- .../NativeFeatureInfoTests.cs | 27 +++++++ .../UnityShaderDecompiler.Tests/SpecBlobs.cs | 16 +--- 6 files changed, 58 insertions(+), 113 deletions(-) create mode 100644 docs/pr-dxbc-feature-flags.md create mode 100644 tests/UnityShaderDecompiler.Tests/NativeFeatureInfoTests.cs diff --git a/Shader Decompiler/DXBC/Chunks/SFI0 Chunk.cs b/Shader Decompiler/DXBC/Chunks/SFI0 Chunk.cs index 423175d..a5cfb70 100644 --- a/Shader Decompiler/DXBC/Chunks/SFI0 Chunk.cs +++ b/Shader Decompiler/DXBC/Chunks/SFI0 Chunk.cs @@ -1,84 +1,15 @@ namespace Parser.DXBC.Chunks; -// D3D11 "SFI0" ShaderInfo0 chunk. Layout matches the well-known -// reverse-engineered d3dcompiler format (SharpDX / Wine / meson sfi0). It -// carries the thread-group sizes (cNumThreads) that the STAT chunk alone -// cannot, and the temp register count cTemps. Shader parser spec §8 lists -// cTemps/cNumThreads as the survive-able shader-info fields. +// SFI0 stores the shader's required feature flags as one 64-bit bitmask. public class Sfi0Chunk { - public uint InputPrimitive; - public uint OutputPrimitive; - public uint MaxInputSignatureRegisterCount; - public uint MaxOutputSignatureRegisterCount; - public uint MaxInputPatchConstantSignatureRegisterCount; - public uint MaxOutputPatchCount; - public uint PatchConstantPrimitive; - public uint CTemps; // temp register count (see spec §8 table) - public uint CIndexableTemp; - public uint CGsInstanceCount; - public uint CControlPoints; - public uint COutputControlPoints; - public uint CTgsm; - public uint CInputGsInstanceCount; - public uint COutputs; - public uint CInputs; - public uint CPatchConst; - public uint CNumThreadsX; // thread-group size X - public uint CNumThreadsY; // thread-group size Y - public uint CNumThreadsZ; // thread-group size Z - public uint CGsOutputTopology; - public uint CInputPrimitive; - public uint CMaxOutputVertices; - public uint COutputTopology; - public uint CMaxTemp; - public uint CMaxOutputPatchCount; - public uint CMaxGsInstanceCount; - public uint CMaxIndexableTemp; - public uint CMaxInputGsInstanceCount; - public uint CMaxOutputs; - public uint CMaxInputs; - public uint CMaxPatchConst; - public uint CMaxNumThreadsX; - public uint CMaxNumThreadsY; - public uint CMaxNumThreadsZ; + public ulong FeatureFlags { get; private set; } public void Read(BinaryReader reader) { - InputPrimitive = reader.ReadUInt32(); - OutputPrimitive = reader.ReadUInt32(); - MaxInputSignatureRegisterCount = reader.ReadUInt32(); - MaxOutputSignatureRegisterCount = reader.ReadUInt32(); - MaxInputPatchConstantSignatureRegisterCount = reader.ReadUInt32(); - MaxOutputPatchCount = reader.ReadUInt32(); - PatchConstantPrimitive = reader.ReadUInt32(); - CTemps = reader.ReadUInt32(); - CIndexableTemp = reader.ReadUInt32(); - CGsInstanceCount = reader.ReadUInt32(); - CControlPoints = reader.ReadUInt32(); - COutputControlPoints = reader.ReadUInt32(); - CTgsm = reader.ReadUInt32(); - CInputGsInstanceCount = reader.ReadUInt32(); - COutputs = reader.ReadUInt32(); - CInputs = reader.ReadUInt32(); - CPatchConst = reader.ReadUInt32(); - CNumThreadsX = reader.ReadUInt32(); - CNumThreadsY = reader.ReadUInt32(); - CNumThreadsZ = reader.ReadUInt32(); - CGsOutputTopology = reader.ReadUInt32(); - CInputPrimitive = reader.ReadUInt32(); - CMaxOutputVertices = reader.ReadUInt32(); - COutputTopology = reader.ReadUInt32(); - CMaxTemp = reader.ReadUInt32(); - CMaxOutputPatchCount = reader.ReadUInt32(); - CMaxGsInstanceCount = reader.ReadUInt32(); - CMaxIndexableTemp = reader.ReadUInt32(); - CMaxInputGsInstanceCount = reader.ReadUInt32(); - CMaxOutputs = reader.ReadUInt32(); - CMaxInputs = reader.ReadUInt32(); - CMaxPatchConst = reader.ReadUInt32(); - CMaxNumThreadsX = reader.ReadUInt32(); - CMaxNumThreadsY = reader.ReadUInt32(); - CMaxNumThreadsZ = reader.ReadUInt32(); + if (reader.BaseStream.Length - reader.BaseStream.Position != sizeof(ulong)) + throw new InvalidDataException("SFI0 chunk must contain exactly 8 bytes of feature flags."); + + FeatureFlags = reader.ReadUInt64(); } } \ No newline at end of file diff --git a/Shader Decompiler/Tests/SpecTestVectors.cs b/Shader Decompiler/Tests/SpecTestVectors.cs index 809f4a0..44bc2e4 100644 --- a/Shader Decompiler/Tests/SpecTestVectors.cs +++ b/Shader Decompiler/Tests/SpecTestVectors.cs @@ -192,7 +192,7 @@ public static void Run() Throws(() => new DxbcFile().Load(lying), "lying TotalLength rejected"); }); - // ---- SFI0 (shader info; cTemps + cNumThreads carriers, spec §8) ---- + // ---- SFI0 (shader feature flags) ---- Case("SFI0 chunk", TestSfi0); // ---- PCON (patch-constant signature) ---- @@ -207,19 +207,17 @@ public static void Run() : $"{s_failures} spec vector(s) FAILED."); } - // ---- SFI0 (spec §8 carrier for cTemps / cNumThreads) ---- + // ---- SFI0 (shader feature flags) ---- private static void TestSfi0() { - var dxbc = DxbcWithChunk("SFI0", Sfi0ChunkBytes(cTemps: 12, tgx: 8, tgy: 4, tgz: 1)); + byte[] dxbc = DxbcWithChunk("SFI0", Sfi0ChunkBytes(2UL)); var file = new DxbcFile(); file.Load(dxbc); Check(file.ShaderInfo is not null, "SFI0 parsed"); if (file.ShaderInfo is { } s) { - Check(s.CTemps == 12, "SFI0 cTemps=12"); - Check(s.CNumThreadsX == 8 && s.CNumThreadsY == 4 && s.CNumThreadsZ == 1, - "SFI0 cNumThreads=(8,4,1)"); + Check(s.FeatureFlags == 2UL, "SFI0 feature flags=2"); Check(file.UnknownChunks.Count == 0, "SFI0 not treated as unknown"); } } @@ -378,18 +376,8 @@ private static byte[] NonComputeBlob(byte texExt, byte cbExt, byte sampExt, byte return blob; } - // SFI0 chunk payload: 35 u32 fields. CTemps at index 7, cNumThreads X/Y/Z - // at indices 17/18/19. - private static byte[] Sfi0ChunkBytes(uint cTemps, uint tgx, uint tgy, uint tgz) - { - var buf = new byte[35 * 4]; - void Set(int idx, uint v) => Array.Copy(BitConverter.GetBytes(v), 0, buf, idx * 4, 4); - Set(7, cTemps); - Set(17, tgx); - Set(18, tgy); - Set(19, tgz); - return buf; - } + // SFI0 chunk payload: one 64-bit shader feature flag mask. + private static byte[] Sfi0ChunkBytes(ulong featureFlags) => BitConverter.GetBytes(featureFlags); // PCON patch-constant signature payload (OSGN element layout). private static byte[] PconChunkBytes() diff --git a/docs/pr-dxbc-feature-flags.md b/docs/pr-dxbc-feature-flags.md new file mode 100644 index 0000000..0863333 --- /dev/null +++ b/docs/pr-dxbc-feature-flags.md @@ -0,0 +1,7 @@ +# Read SFI0 as a 64-bit feature mask + +The SFI0 reader previously treated the payload as a statistics block. Read exactly eight bytes, reject invalid lengths, and verify the feature mask against native compiler output. The existing synthetic test is corrected because its old payload represented the wrong chunk format. + +Validation: 53 C# tests and all four existing PicaVoxel extraction/decompilation tests pass. C# tests use an en-US default culture; the upstream fr-FR golden-output failure is addressed independently by the numeric-formatting PR. + +This branch is based directly on upstream `1850608`; it does not include the other review branches. No proprietary fixtures or machine-specific paths are added. diff --git a/tests/UnityShaderDecompiler.Tests/BlobAndDxbcTests.cs b/tests/UnityShaderDecompiler.Tests/BlobAndDxbcTests.cs index f4bf2a6..ad20884 100644 --- a/tests/UnityShaderDecompiler.Tests/BlobAndDxbcTests.cs +++ b/tests/UnityShaderDecompiler.Tests/BlobAndDxbcTests.cs @@ -211,22 +211,26 @@ public void TotalLengthShorterThanChunks_IsRejected() // ── SFI0 / PCON / compute blob ──────────────────────────────────── [Fact] - public void Sfi0Chunk_ExposesTempAndThreadCounts() + public void Sfi0Chunk_ExposesFeatureFlags() { var dxbc = SpecBlobs.DxbcWithChunk( - "SFI0", SpecBlobs.Sfi0ChunkBytes(cTemps: 12, tgx: 8, tgy: 4, tgz: 1)); + "SFI0", SpecBlobs.Sfi0ChunkBytes(2UL)); var file = new DxbcFile(); file.Load(dxbc); Assert.NotNull(file.ShaderInfo); var s = file.ShaderInfo!; - Assert.Equal(12u, s.CTemps); - Assert.Equal(8u, s.CNumThreadsX); - Assert.Equal(4u, s.CNumThreadsY); - Assert.Equal(1u, s.CNumThreadsZ); + Assert.Equal(2UL, s.FeatureFlags); Assert.Empty(file.UnknownChunks); } + [Fact] + public void Sfi0Chunk_RejectsInvalidLength() + { + byte[] dxbc = SpecBlobs.DxbcWithChunk("SFI0", new byte[4]); + Assert.Throws(() => new DxbcFile().Load(dxbc)); + } + [Fact] public void PconChunk_ExposesPatchConstantSignature() { diff --git a/tests/UnityShaderDecompiler.Tests/NativeFeatureInfoTests.cs b/tests/UnityShaderDecompiler.Tests/NativeFeatureInfoTests.cs new file mode 100644 index 0000000..d05c1f3 --- /dev/null +++ b/tests/UnityShaderDecompiler.Tests/NativeFeatureInfoTests.cs @@ -0,0 +1,27 @@ +using System; +using System.Buffers.Binary; +using Parser; +using Parser.DXBC; + +namespace UnityShaderDecompiler.Tests; + +public class NativeFeatureInfoTests +{ + [Fact] + [Trait("Category", "D3D")] + public void FeatureFlagsMatchTheNativeCompilersEightBytePayload() + { + if (!OperatingSystem.IsWindows()) return; + + const string source = "float main(float x : TEXCOORD) : SV_Target " + + "{ double value = (double)x; return (float)(value * value); }"; + byte[] bytes = RecompileVerify.CompileHlsl(source, "main", "ps_5_0"); + DxbcFile compiled = new(); + compiled.Load(bytes); + DxbcChunk featureInfo = Assert.Single(compiled.Chunks, chunk => chunk.Name.ToString() == "SFI0"); + Assert.Equal(8, featureInfo.Data.Length); + Assert.Equal(1UL, BinaryPrimitives.ReadUInt64LittleEndian(featureInfo.Data)); + Assert.NotNull(compiled.ShaderInfo); + Assert.Equal(1UL, compiled.ShaderInfo.FeatureFlags); + } +} diff --git a/tests/UnityShaderDecompiler.Tests/SpecBlobs.cs b/tests/UnityShaderDecompiler.Tests/SpecBlobs.cs index 6658195..a3746e2 100644 --- a/tests/UnityShaderDecompiler.Tests/SpecBlobs.cs +++ b/tests/UnityShaderDecompiler.Tests/SpecBlobs.cs @@ -69,20 +69,8 @@ public static byte[] NonComputeBlob(byte texExt, byte cbExt, byte sampExt, byte return blob; } - /// - /// SFI0 chunk payload: 35 u32 fields. CTemps at index 7, cNumThreads - /// X/Y/Z at indices 17/18/19. - /// - public static byte[] Sfi0ChunkBytes(uint cTemps, uint tgx, uint tgy, uint tgz) - { - var buf = new byte[35 * 4]; - void Set(int idx, uint v) => Array.Copy(BitConverter.GetBytes(v), 0, buf, idx * 4, 4); - Set(7, cTemps); - Set(17, tgx); - Set(18, tgy); - Set(19, tgz); - return buf; - } + /// SFI0 chunk payload: one 64-bit shader feature flag mask. + public static byte[] Sfi0ChunkBytes(ulong featureFlags) => BitConverter.GetBytes(featureFlags); /// PCON patch-constant signature payload (OSGN element layout). public static byte[] PconChunkBytes() From 7e7e5857ed4e0b8dfde3b190bf90137b31633fa3 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 12:14:11 +0100 Subject: [PATCH 03/93] Propagate DirectX subprogram decompilation failures --- Shader Decompiler/Decompiler.cs | 6 +++ docs/pr-directx-fail-fast.md | 7 +++ .../DirectXFailureTests.cs | 47 +++++++++++++++++++ 3 files changed, 60 insertions(+) create mode 100644 docs/pr-directx-fail-fast.md create mode 100644 tests/UnityShaderDecompiler.Tests/DirectXFailureTests.cs diff --git a/Shader Decompiler/Decompiler.cs b/Shader Decompiler/Decompiler.cs index 5a44dad..47df062 100644 --- a/Shader Decompiler/Decompiler.cs +++ b/Shader Decompiler/Decompiler.cs @@ -341,6 +341,12 @@ public static DecompilationResult Decompile( } catch (Exception ex) { + if (sp.m_ProgramType is >= ShaderGpuProgramType.DX11VertexSM40 + and <= ShaderGpuProgramType.DX11DomainSM50) + { + throw new InvalidDataException( + $"Failed to decompile DirectX subprogram {i} ({sp.m_ProgramType}): {ex.Message}", ex); + } log.WriteLine($"No DXBC: {ex.Message}"); } diff --git a/docs/pr-directx-fail-fast.md b/docs/pr-directx-fail-fast.md new file mode 100644 index 0000000..2bc9d25 --- /dev/null +++ b/docs/pr-directx-fail-fast.md @@ -0,0 +1,7 @@ +# Propagate DirectX subprogram decompilation failures + +A malformed DirectX program was logged as "No DXBC" and omitted from an apparently successful export. Propagate the failure with the program index and stage, retaining its original cause. A synthetic malformed program verifies the pipeline fails before writing a shader. + +Validation: 53 C# tests and all four existing PicaVoxel extraction/decompilation tests pass. C# tests use an en-US default culture; the upstream fr-FR golden-output failure is addressed independently by the numeric-formatting PR. + +This branch is based directly on upstream `1850608`; it does not include the other review branches. No proprietary fixtures or machine-specific paths are added. diff --git a/tests/UnityShaderDecompiler.Tests/DirectXFailureTests.cs b/tests/UnityShaderDecompiler.Tests/DirectXFailureTests.cs new file mode 100644 index 0000000..f7fda00 --- /dev/null +++ b/tests/UnityShaderDecompiler.Tests/DirectXFailureTests.cs @@ -0,0 +1,47 @@ +using System.IO; +using AssetStudio; +using Parser; + +namespace UnityShaderDecompiler.Tests; + +public class DirectXFailureTests +{ + [Theory] + [InlineData(ShaderGpuProgramType.DX11VertexSM40)] + [InlineData(ShaderGpuProgramType.DX11PixelSM50)] + public void MalformedDirectXProgramStopsBeforeWritingTheShader(ShaderGpuProgramType stage) + { + using TempDirectory folder = new(); + using MemoryStream payload = new(); + using (BinaryWriter writer = new(payload, System.Text.Encoding.UTF8, leaveOpen: true)) + { + writer.Write(202012090); + writer.Write((int)stage); + writer.Write(new byte[16]); + writer.Write(0); // keyword count + writer.Write(4); // deliberately incomplete shader code + writer.Write("DXBC"u8); + } + using MemoryStream blob = new(); + using (BinaryWriter writer = new(blob, System.Text.Encoding.UTF8, leaveOpen: true)) + { + writer.Write(1); // one subprogram, starting after the entry table + writer.Write(16); + writer.Write((int)payload.Length); + writer.Write(0); + writer.Write(payload.ToArray()); + } + File.WriteAllBytes(Path.Combine(folder.Path, "blob.bin"), blob.ToArray()); + File.WriteAllText(Path.Combine(folder.Path, "metadata.json"), + "{\"name\":\"Test/InvalidDxbc\",\"subShaders\":[]}"); + string output = Path.Combine(folder.Path, "generated"); + + InvalidDataException error = Assert.Throws(() => + Decompiler.DecompileFolder(folder.Path, new DecompileOptions { Verbose = false, OutRoot = output })); + + Assert.Contains("DirectX subprogram 0", error.Message); + Assert.Contains(stage.ToString(), error.Message); + Assert.NotNull(error.InnerException); + Assert.Empty(Directory.GetFiles(output, "*.shader")); + } +} From bbff9a58063bed2cd7b2cac14715c7051f741827 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 12:14:44 +0100 Subject: [PATCH 04/93] Use invariant numeric literals in shader output --- .../Core/Hlsl/HlslPrettyPrinter.cs | 13 ++-- Shader Decompiler/DXBC/IR/IRExpression.cs | 11 ++-- docs/pr-invariant-numeric-output.md | 7 ++ .../InvariantNumericOutputTests.cs | 64 +++++++++++++++++++ 4 files changed, 84 insertions(+), 11 deletions(-) create mode 100644 docs/pr-invariant-numeric-output.md create mode 100644 tests/UnityShaderDecompiler.Tests/InvariantNumericOutputTests.cs diff --git a/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs b/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs index 222df18..1a8cc87 100644 --- a/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs +++ b/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs @@ -1,3 +1,4 @@ +using System.Globalization; using System.Text; using System.Text.Json; using System.Text.RegularExpressions; @@ -87,7 +88,7 @@ private static void PrintProperties(StringBuilder sb, List pro HlslPropertyKind.Color => "Color", HlslPropertyKind.Vector => "Vector", HlslPropertyKind.Float => "Float", - HlslPropertyKind.Range when p.Range is { } r => $"Range({r.Min}, {r.Max})", + HlslPropertyKind.Range when p.Range is { } r => FormattableString.Invariant($"Range({r.Min}, {r.Max})"), // Range with no recovered bounds (metadata carried no default value, // so min/max slots are absent) — emit as Float rather than // fabricate a (0,1) that might be wrong. @@ -115,8 +116,8 @@ private static string PropertyDefaultText(HlslPropertyNode p) float x = p.DefaultComponents[0], y = p.DefaultComponents[1], z = p.DefaultComponents[2], w = p.DefaultComponents[3]; return p.Kind is HlslPropertyKind.Color or HlslPropertyKind.Vector - ? $"({x},{y},{z},{w})" - : x.ToString(); + ? FormattableString.Invariant($"({x},{y},{z},{w})") + : x.ToString("R", CultureInfo.InvariantCulture); } if (p.DefaultValue is not { } dv || dv.ValueKind != JsonValueKind.Object) @@ -128,8 +129,8 @@ private static string PropertyDefaultText(HlslPropertyNode p) double W() => dv.TryGetProperty("w", out var w) ? w.GetDouble() : 0; return p.Kind is HlslPropertyKind.Color or HlslPropertyKind.Vector - ? $"({X()},{Y()},{Z()},{W()})" - : X().ToString(); + ? FormattableString.Invariant($"({X()},{Y()},{Z()},{W()})") + : X().ToString("R", CultureInfo.InvariantCulture); } // ---------- Stage 5: SubShader ---------- @@ -233,7 +234,7 @@ private static void PrintRenderState(StringBuilder sb, HlslRenderState s) sb.Append(" AlphaToMask On\n"); if (s.OffsetFactor != 0 || s.OffsetUnits != 0) - sb.Append(" Offset ").Append(s.OffsetFactor).Append(", ").Append(s.OffsetUnits).Append('\n'); + sb.Append(" Offset ").Append(s.OffsetFactor.ToString("R", CultureInfo.InvariantCulture)).Append(", ").Append(s.OffsetUnits.ToString("R", CultureInfo.InvariantCulture)).Append('\n'); if (!(s.Blend.SrcBlend == HlslBlendMode.One && s.Blend.DstBlend == HlslBlendMode.Zero)) { diff --git a/Shader Decompiler/DXBC/IR/IRExpression.cs b/Shader Decompiler/DXBC/IR/IRExpression.cs index 1be6411..4daf48f 100644 --- a/Shader Decompiler/DXBC/IR/IRExpression.cs +++ b/Shader Decompiler/DXBC/IR/IRExpression.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.Linq; using System.Collections.Generic; @@ -55,8 +56,8 @@ public override string ToString() if (Kind == ConstantKind.Double) { return DoubleValues.Length == 1 - ? DoubleValues[0].ToString() - : $"double{DoubleValues.Length}({string.Join(", ", DoubleValues)})"; + ? DoubleValues[0].ToString("R", CultureInfo.InvariantCulture) + : $"double{DoubleValues.Length}({string.Join(", ", DoubleValues.Select(v => v.ToString("R", CultureInfo.InvariantCulture)))})"; } string Format(uint value) @@ -64,13 +65,13 @@ string Format(uint value) return Kind switch { ConstantKind.Float => - BitConverter.Int32BitsToSingle((int)value).ToString(), + BitConverter.Int32BitsToSingle((int)value).ToString("R", CultureInfo.InvariantCulture), ConstantKind.Int => - unchecked((int)value).ToString(), + unchecked((int)value).ToString(CultureInfo.InvariantCulture), ConstantKind.UInt => - value.ToString(), + value.ToString(CultureInfo.InvariantCulture), _ => "?" }; diff --git a/docs/pr-invariant-numeric-output.md b/docs/pr-invariant-numeric-output.md new file mode 100644 index 0000000..08c1df3 --- /dev/null +++ b/docs/pr-invariant-numeric-output.md @@ -0,0 +1,7 @@ +# Use invariant numeric literals in shader output + +ShaderLab properties, render-state offsets and HLSL constants must use decimal points regardless of the Windows locale. Preserve round-trip float precision. Regression cases exercise French and German cultures. + +Validation: 53 C# tests and all four existing PicaVoxel extraction/decompilation tests pass. + +This branch is based directly on upstream `1850608`; it does not include the other review branches. No proprietary fixtures or machine-specific paths are added. diff --git a/tests/UnityShaderDecompiler.Tests/InvariantNumericOutputTests.cs b/tests/UnityShaderDecompiler.Tests/InvariantNumericOutputTests.cs new file mode 100644 index 0000000..da4f43c --- /dev/null +++ b/tests/UnityShaderDecompiler.Tests/InvariantNumericOutputTests.cs @@ -0,0 +1,64 @@ +using System; +using System.Linq; +using Parser.Core.Hlsl; +using Parser.Core.Hlsl.Ast; +using Parser.Core.Analysis; +using Parser.Core.Optimizations; +using Parser.DXBC.Instructions; +using Parser.DXBC.IR; +using Xunit; + +namespace UnityShaderDecompiler.Tests; + +public sealed class InvariantNumericOutputTests +{ + [Theory] + [InlineData("fr-FR")] + [InlineData("de-DE")] + public void NumericConstantsUseInvariantDecimalSeparators(string culture) + { + System.Globalization.CultureInfo previous = System.Globalization.CultureInfo.CurrentCulture; + try + { + System.Globalization.CultureInfo.CurrentCulture = new(culture); + Assert.Equal("0.125", Float(0.125f).ToString()); + IRExpression.ConstantExpression vector = new IRExpression.ConstantExpression + { + Kind = IRExpression.ConstantExpression.ConstantKind.Double, + DoubleValues = new[] { 0.125, 1.25 }, + }; + Assert.Equal("double2(0.125, 1.25)", vector.ToString()); + } + finally { System.Globalization.CultureInfo.CurrentCulture = previous; } + } + + private static string Print(HlslFunctionNode function) + { + HlslShaderNode shader = new HlslShaderNode { Name = "Test/Scope" }; + HlslSubShaderNode subShader = new HlslSubShaderNode(); + subShader.Passes.Add(new HlslPassNode { FragmentFunction = function }); + shader.SubShaders.Add(subShader); + return HlslPrettyPrinter.Print(shader, fuseTemps: false); + } + + private static IRRegister Temp(uint index, byte mask = 1) + { + IRRegister register = new IRRegister + { + RegisterType = RegisterType.Temp, + Index = index, + Type = IRValueType.Float, + Mask = mask, + ComponentMode = Operand.OperandComponentMode.Mask, + }; + for (int component = 0; component < 4; component++) + if ((mask & (1 << component)) != 0) + register.SsaVersion[component] = 1; + return register; + } + + private static IRExpression.ConstantExpression Float(float value) => new() + { + RawValues = new[] { unchecked((uint)BitConverter.SingleToInt32Bits(value)) }, + }; +} From 76245b4dadfb2eba2085d9b44f52813aa38fffb5 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 12:15:57 +0100 Subject: [PATCH 05/93] Preserve structured resource strides and load offsets --- Shader Decompiler/Core/Hlsl/HlslAST.cs | 2 + Shader Decompiler/Core/Hlsl/HlslAstBuilder.cs | 13 +++ .../Core/Hlsl/HlslPrettyPrinter.cs | 42 ++++++- .../Optimizations/IRExpressionRewriter.cs | 2 + .../DXBC/IR/Builders/IRBuilderTexture.cs | 29 ++--- Shader Decompiler/DXBC/IR/IRExpression.cs | 5 + docs/pr-structured-buffer-loads.md | 7 ++ .../StructuredBufferLoadsTests.cs | 104 ++++++++++++++++++ 8 files changed, 189 insertions(+), 15 deletions(-) create mode 100644 docs/pr-structured-buffer-loads.md create mode 100644 tests/UnityShaderDecompiler.Tests/StructuredBufferLoadsTests.cs diff --git a/Shader Decompiler/Core/Hlsl/HlslAST.cs b/Shader Decompiler/Core/Hlsl/HlslAST.cs index 93478c0..ce20390 100644 --- a/Shader Decompiler/Core/Hlsl/HlslAST.cs +++ b/Shader Decompiler/Core/Hlsl/HlslAST.cs @@ -299,6 +299,8 @@ public sealed class HlslResourceNode // otherwise rather than guessing. public string? TypeHint { get; init; } + public uint StructureStride { get; init; } + // Only populated for Kind == ConstantBuffer, from RDEF (the actual // member layout Unity's compiler produced) — empty if no RDEF match // was found, not an error, just means Stage 7's CBUFFER_START/END diff --git a/Shader Decompiler/Core/Hlsl/HlslAstBuilder.cs b/Shader Decompiler/Core/Hlsl/HlslAstBuilder.cs index 1472174..64f2f0a 100644 --- a/Shader Decompiler/Core/Hlsl/HlslAstBuilder.cs +++ b/Shader Decompiler/Core/Hlsl/HlslAstBuilder.cs @@ -318,6 +318,19 @@ public static IEnumerable BuildResources( }; break; + case IRDeclaration.IRResourceStructuredDeclaration structured: + if (structured.StructureStride == 0 || structured.StructureStride % 4 != 0) + throw new InvalidDataException($"Invalid structured resource stride {structured.StructureStride} at t{structured.Slot}."); + yield return new HlslResourceNode + { + Name = $"t{structured.Slot}", + Kind = HlslResourceKind.Texture, + Slot = structured.Slot, + TypeHint = "StructuredBuffer", + StructureStride = structured.StructureStride, + }; + break; + case IRDeclaration.IRSamplerDeclaration samp: bool isComparison = ComparisonSamplers(blocks).Contains(samp.Slot); // Unity pairs samplers with textures by naming convention: diff --git a/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs b/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs index 222df18..255a9e1 100644 --- a/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs +++ b/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs @@ -506,7 +506,15 @@ private static void PrintResource(StringBuilder sb, HlslResourceNode res, bool s break; case HlslResourceKind.Texture: - sb.Append(" ").Append(res.TypeHint ?? "Texture2D").Append(' ').Append(res.Name).Append(regSuffix).Append(";\n"); + if (res.StructureStride > 0) + { + sb.Append(" struct ").Append(res.Name).Append("_element { uint value[") + .Append(res.StructureStride / 4).Append("]; };\n"); + sb.Append(" StructuredBuffer<").Append(res.Name).Append("_element> ") + .Append(res.Name).Append(regSuffix).Append(";\n"); + } + else + sb.Append(" ").Append(res.TypeHint ?? "Texture2D").Append(' ').Append(res.Name).Append(regSuffix).Append(";\n"); break; case HlslResourceKind.Sampler: @@ -2281,7 +2289,9 @@ private static string TrimToWidth(string rhs, List active, IRExpression exp IRExpression.DotProductExpression => 1, IRExpression.SwizzleExpression s => Math.Max(1, s.Components.Count), IRExpression.MatrixVectorMultiplyExpression mv => Math.Max(1, mv.Rows.Count), - IRExpression.TextureOperationExpression tex => TextureOpWidth(tex.Operation), + IRExpression.TextureOperationExpression tex => tex.Operation == IRExpression.TextureOperation.LoadStructured + ? Math.Max(1, MaskIndices(tex.DestinationMask).Count) + : TextureOpWidth(tex.Operation), _ => 1, }; @@ -2691,6 +2701,9 @@ private static string RenderMatrixMultiply(IRExpression.MatrixVectorMultiplyExpr // needed fixing up from the debug ToString(). private static string RenderTextureOp(IRExpression.TextureOperationExpression tex, PrintContext ctx) { + if (tex.Operation == IRExpression.TextureOperation.LoadStructured) + return RenderStructuredLoad(tex, ctx); + string resource = RenderRegisterRead(tex.Resource, ctx); string? sampler = tex.Sampler is null ? null : RenderRegisterRead(tex.Sampler, ctx); string? coord = tex.Coordinates is null ? null : RenderExpression(tex.Coordinates, ctx); @@ -2743,4 +2756,29 @@ private static string RenderTextureOp(IRExpression.TextureOperationExpression te $"/* TODO: no confirmed HLSL mapping for {tex.Operation} */ {resource}.{tex.Operation}({Args(sampler, coord)})", }; } + + private static string RenderStructuredLoad(IRExpression.TextureOperationExpression tex, PrintContext ctx) + { + if (tex.Coordinates is null || tex.Offset is not IRExpression.ConstantExpression { RawValues.Length: 1 } offset) + throw new InvalidDataException("Structured load requires an element index and a constant byte offset."); + uint byteOffset = offset.RawValues[0]; + if (byteOffset % 4 != 0) + throw new InvalidDataException($"Unaligned structured load offset {byteOffset}."); + + List lanes = MaskIndices(tex.DestinationMask); + if (lanes.Count == 0) + throw new InvalidDataException("Structured load has no destination lanes."); + List swizzle = ActiveComponents(tex.Resource); + List values = new List(); + foreach (int lane in lanes) + { + uint word = byteOffset / 4 + (uint)swizzle[lane]; + if (word >= tex.StructuredStride / 4) + throw new InvalidDataException($"Structured load exceeds element stride at t{tex.Resource.Index}."); + values.Add($"t{tex.Resource.Index}[{RenderExpression(tex.Coordinates, ctx)}].value[{word}]"); + } + return lanes.Count == 1 + ? $"asfloat({values[0]})" + : $"asfloat(uint{lanes.Count}({string.Join(", ", values)}))"; + } } diff --git a/Shader Decompiler/Core/Optimizations/IRExpressionRewriter.cs b/Shader Decompiler/Core/Optimizations/IRExpressionRewriter.cs index cbb1b23..b7ab903 100644 --- a/Shader Decompiler/Core/Optimizations/IRExpressionRewriter.cs +++ b/Shader Decompiler/Core/Optimizations/IRExpressionRewriter.cs @@ -105,6 +105,8 @@ public static class IRExpressionRewriter Sampler = tex.Sampler, Coordinates = Rewrite(tex.Coordinates, transform), Offset = Rewrite(tex.Offset, transform), + StructuredStride = tex.StructuredStride, + DestinationMask = tex.DestinationMask, LOD = Rewrite(tex.LOD, transform), Bias = Rewrite(tex.Bias, transform), CompareValue = Rewrite(tex.CompareValue, transform), diff --git a/Shader Decompiler/DXBC/IR/Builders/IRBuilderTexture.cs b/Shader Decompiler/DXBC/IR/Builders/IRBuilderTexture.cs index 45edbd9..f0140db 100644 --- a/Shader Decompiler/DXBC/IR/Builders/IRBuilderTexture.cs +++ b/Shader Decompiler/DXBC/IR/Builders/IRBuilderTexture.cs @@ -40,6 +40,7 @@ private void BuildTextureOp( Sampler = sampler, Coordinates = coordinates, Offset = offset, + DestinationMask = destination.Mask, LOD = lod, Bias = bias, CompareValue = compareValue, @@ -96,19 +97,21 @@ private void BuildLdRaw(IRProgram program, Instruction instruction) // ld_structured: operand 1 = element index, operand 2 = byte offset within element private void BuildLdStructured(IRProgram program, Instruction instruction) { - IRExpression address = - new IRExpression.BinaryExpression - { - Operation = IRExpression.BinaryOperation.Add, - Left = BuildUIntExpression(instruction.Operands[1]), - Right = BuildUIntExpression(instruction.Operands[2]) - }; - - BuildTextureOp( - program, instruction, IRExpression.TextureOperation.Load, - destination: BuildRegister(instruction.Operands[0]), - resource: BuildRegister(instruction.Operands[3]), - coordinates: address); + IRRegister destination = BuildRegister(instruction.Operands[0]); + IRRegister resource = BuildRegister(instruction.Operands[3]); + uint stride = program.Declarations.OfType() + .Single(d => d.Slot == resource.Index).StructureStride; + if (stride == 0 || stride % 4 != 0) + throw new InvalidDataException($"Invalid structured resource stride {stride} at t{resource.Index}."); + AddAssignment(program, destination, new IRExpression.TextureOperationExpression + { + Operation = IRExpression.TextureOperation.LoadStructured, + Resource = resource, + Coordinates = BuildUIntExpression(instruction.Operands[1]), + Offset = BuildUIntExpression(instruction.Operands[2]), + StructuredStride = stride, + DestinationMask = destination.Mask, + }); } // ============================================================ diff --git a/Shader Decompiler/DXBC/IR/IRExpression.cs b/Shader Decompiler/DXBC/IR/IRExpression.cs index 1be6411..b7ec646 100644 --- a/Shader Decompiler/DXBC/IR/IRExpression.cs +++ b/Shader Decompiler/DXBC/IR/IRExpression.cs @@ -517,6 +517,7 @@ public enum TextureOperation SampleCompare, SampleCompareLevelZero, Load, + LoadStructured, Gather, GatherCompare, Lod, @@ -539,6 +540,10 @@ public sealed class TextureOperationExpression : IRExpression public IRExpression? Offset { get; init; } + public uint StructuredStride { get; init; } + + public byte DestinationMask { get; init; } + public IRExpression? LOD { get; init; } public IRExpression? Bias { get; init; } diff --git a/docs/pr-structured-buffer-loads.md b/docs/pr-structured-buffer-loads.md new file mode 100644 index 0000000..4bdf31d --- /dev/null +++ b/docs/pr-structured-buffer-loads.md @@ -0,0 +1,7 @@ +# Preserve structured resource strides and load offsets + +Represent structured resources with their declared stride and lower structured loads using the element index, byte offset and resource swizzle. Reject invalid strides and offsets instead of guessing a layout. + +Validation: 55 C# tests and all four existing PicaVoxel extraction/decompilation tests pass. C# tests use an en-US default culture; the upstream fr-FR golden-output failure is addressed independently by the numeric-formatting PR. + +This branch is based directly on upstream `1850608`; it does not include the other review branches. No proprietary fixtures or machine-specific paths are added. diff --git a/tests/UnityShaderDecompiler.Tests/StructuredBufferLoadsTests.cs b/tests/UnityShaderDecompiler.Tests/StructuredBufferLoadsTests.cs new file mode 100644 index 0000000..cb01e4f --- /dev/null +++ b/tests/UnityShaderDecompiler.Tests/StructuredBufferLoadsTests.cs @@ -0,0 +1,104 @@ +using System; +using System.Linq; +using Parser.Core.Hlsl; +using Parser.Core.Hlsl.Ast; +using Parser.Core.Analysis; +using Parser.Core.Optimizations; +using Parser.DXBC.Instructions; +using Parser.DXBC.IR; +using Xunit; + +namespace UnityShaderDecompiler.Tests; + +public sealed class StructuredBufferLoadsTests +{ + [Theory] + [InlineData(4u, 32u, true)] + [InlineData(2u, 32u, false)] + [InlineData(32u, 32u, false)] + public void StructuredLoadUsesByteOffsetAndResourceSwizzle(uint offset, uint stride, bool valid) + { + IRRegister resource = new() + { + RegisterType = RegisterType.Resource, Index = 7, + ComponentMode = Operand.OperandComponentMode.Swizzle, + Swizzle = 2 | (0 << 2) | (1 << 4) | (3 << 6), + }; + HlslFunctionNode function = new() { Name = "frag", Stage = HlslShaderStage.Fragment }; + function.Statements.Statements.Add(new HlslAssignmentStatement + { + Destination = Temp(0, 3), + Expression = new IRExpression.TextureOperationExpression + { + Operation = IRExpression.TextureOperation.LoadStructured, + Resource = resource, StructuredStride = stride, DestinationMask = 3, + Coordinates = new IRExpression.ConstantExpression + { + Kind = IRExpression.ConstantExpression.ConstantKind.UInt, RawValues = new uint[] { 5 }, + }, + Offset = new IRExpression.ConstantExpression + { + Kind = IRExpression.ConstantExpression.ConstantKind.UInt, RawValues = new uint[] { offset }, + }, + }, + }); + if (valid) + { + string source = Print(function); + Assert.Contains("t7[5].value[3], t7[5].value[1]", source); + } + else + { + Assert.Throws(() => Print(function)); + } + } + + [Fact] + public void StructuredBufferDeclarationUsesItsActualStride() + { + HlslShaderNode shader = new HlslShaderNode { Name = "Test/Structured" }; + HlslSubShaderNode subShader = new HlslSubShaderNode(); + HlslPassNode pass = new HlslPassNode(); + pass.Resources.Add(new HlslResourceNode + { + Name = "t7", Kind = HlslResourceKind.Texture, Slot = 7, + TypeHint = "StructuredBuffer", StructureStride = 32, + }); + subShader.Passes.Add(pass); + shader.SubShaders.Add(subShader); + + string source = HlslPrettyPrinter.Print(shader, fuseTemps: false); + Assert.Contains("uint value[8];", source); + Assert.Contains("StructuredBuffer t7", source); + } + + private static string Print(HlslFunctionNode function) + { + HlslShaderNode shader = new HlslShaderNode { Name = "Test/Scope" }; + HlslSubShaderNode subShader = new HlslSubShaderNode(); + subShader.Passes.Add(new HlslPassNode { FragmentFunction = function }); + shader.SubShaders.Add(subShader); + return HlslPrettyPrinter.Print(shader, fuseTemps: false); + } + + private static IRRegister Temp(uint index, byte mask = 1) + { + IRRegister register = new IRRegister + { + RegisterType = RegisterType.Temp, + Index = index, + Type = IRValueType.Float, + Mask = mask, + ComponentMode = Operand.OperandComponentMode.Mask, + }; + for (int component = 0; component < 4; component++) + if ((mask & (1 << component)) != 0) + register.SsaVersion[component] = 1; + return register; + } + + private static IRExpression.ConstantExpression Float(float value) => new() + { + RawValues = new[] { unchecked((uint)BitConverter.SingleToInt32Bits(value)) }, + }; +} From 55002868d039f2b8018a21d892d7641b278762ae Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 12:16:26 +0100 Subject: [PATCH 06/93] Preserve values across conditional control flow --- Shader Decompiler/Core/Analysis/IRLeaveSsa.cs | 6 +- .../Core/Hlsl/HlslPrettyPrinter.cs | 47 +++++--- Shader Decompiler/Output/HOLO_Holo.shader | 6 +- docs/pr-conditional-value-scope.md | 7 ++ .../ConditionalValueScopeTests.cs | 111 ++++++++++++++++++ 5 files changed, 159 insertions(+), 18 deletions(-) create mode 100644 docs/pr-conditional-value-scope.md create mode 100644 tests/UnityShaderDecompiler.Tests/ConditionalValueScopeTests.cs diff --git a/Shader Decompiler/Core/Analysis/IRLeaveSsa.cs b/Shader Decompiler/Core/Analysis/IRLeaveSsa.cs index 34e014a..dc35939 100644 --- a/Shader Decompiler/Core/Analysis/IRLeaveSsa.cs +++ b/Shader Decompiler/Core/Analysis/IRLeaveSsa.cs @@ -29,7 +29,7 @@ public static void Run(List blocks) foreach (IRStatement.IRPhi phi in phis) { if (p >= phi.Operands.Count) - continue; // malformed phi (IRSsaVerifier would've flagged it) — skip rather than crash + throw new InvalidDataException("Phi has fewer operands than predecessors."); copies.Add(new IRStatement.IRAssignment { @@ -71,6 +71,10 @@ private static IRBlock ResolveInsertionTarget(IRBlock pred) { while (pred.Statements.Count == 1 && IsMarker(pred.Statements[0])) { + // Closing a conditional joins both paths. Copies appended here + // must stay after the marker, rather than move into one branch. + if (pred.Statements[0] is IRStatement.IREndIf or IRStatement.IREndSwitch) + break; if (pred.Predecessors.Count == 0) break; // nothing real to redirect to (entry block) — leave as-is diff --git a/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs b/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs index 222df18..39f49b4 100644 --- a/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs +++ b/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs @@ -1475,12 +1475,7 @@ private static void PrintStatement(StringBuilder sb, HlslStatementNode stmt, int { case HlslIfStatement iff: { - // Cross-branch phi copies (IRLeaveSsa writes the same - // post-merge name at the top level of BOTH arms) must have - // their declaration hoisted above the if. Declared per-arm, - // the name is scoped to that arm's braces and dies at the - // `}` — the sibling arm's write and any post-merge read - // would then reference an identifier never in scope. + // Declare branch-written values outside the if so later reads remain in scope. foreach ((string name, int comps) in CrossBranchMergeNames(iff, ctx)) { if (!ctx.Declared.Add(name)) @@ -2355,22 +2350,44 @@ or IRExpression.TextureOperation.SampleCompareLevelZero // HlslIfStatement print case). Returns (rendered name, component count // for the declaration type), mirroring the exact naming/typing // RenderAssignmentLines will produce when it prints those same writes. - private static List<(string Name, int Components)> CrossBranchMergeNames(HlslIfStatement iff, PrintContext ctx) + private static Dictionary CrossBranchMergeNames(HlslIfStatement iff, PrintContext ctx) { - var result = new List<(string, int)>(); + Dictionary result = new Dictionary(); if (iff.Else is null) return result; - - Dictionary thenNames = WrittenDeclNames(iff.Then.Statements, ctx); - Dictionary elseNames = WrittenDeclNames(iff.Else.Statements, ctx); - + Dictionary thenNames = new Dictionary(); + Dictionary elseNames = new Dictionary(); + AddNames(iff.Then, thenNames, ctx); + AddNames(iff.Else, elseNames, ctx); foreach ((string name, int comps) in thenNames) - if (elseNames.TryGetValue(name, out int elseComps)) - result.Add((name, Math.Max(comps, elseComps))); - + result[name] = comps; + foreach ((string name, int comps) in elseNames) + result[name] = Math.Max(result.GetValueOrDefault(name), comps); return result; } + private static void AddNames(HlslBlockStatement block, Dictionary names, PrintContext ctx) + { + foreach ((string name, int comps) in WrittenDeclNames(block.Statements, ctx)) + if (!names.TryGetValue(name, out int prev) || comps > prev) + names[name] = comps; + + foreach (HlslStatementNode stmt in block.Statements) + switch (stmt) + { + case HlslIfStatement nested: + AddNames(nested.Then, names, ctx); + if (nested.Else is not null) AddNames(nested.Else, names, ctx); + break; + case HlslLoopStatement loop: + AddNames(loop.Body, names, ctx); + break; + case HlslSwitchStatement sw: + foreach (HlslSwitchCase c in sw.Cases) AddNames(c.Body, names, ctx); + break; + } + } + // Names first written at the top level of a loop body that are not yet // in scope (see the HlslLoopStatement print case). Only names not // already declared are candidates — an existing declaration is already diff --git a/Shader Decompiler/Output/HOLO_Holo.shader b/Shader Decompiler/Output/HOLO_Holo.shader index 95947ba..e6593c5 100644 --- a/Shader Decompiler/Output/HOLO_Holo.shader +++ b/Shader Decompiler/Output/HOLO_Holo.shader @@ -269,11 +269,13 @@ Shader "HOLO/Holo" float4 r0_xyzw_5 = (_OriginalUVSwitch.xxxx == float4(0, 0, 0, 1)); float r0_y_5 = r0_xyzw_5.y; float r0_w_3 = r0_xyzw_5.w; + float2 uvDiffuse_yz_3; + float4 sampleDiffuse_xyzw_2; float4 sampleDiffuse_xyzw_3; if (r0_y_5) { - float2 uvDiffuse_yz_3 = (mad(i.texcoord2.xxyx, _Diffuse_ST.yyyy, _Diffuse_ST.wwww)).yz; - float4 sampleDiffuse_xyzw_2 = _Diffuse.Sample(sampler_Diffuse, (uvDiffuse_yz_3.xyxx).xy); + uvDiffuse_yz_3 = (mad(i.texcoord2.xxyx, _Diffuse_ST.yyyy, _Diffuse_ST.wwww)).yz; + sampleDiffuse_xyzw_2 = _Diffuse.Sample(sampler_Diffuse, (uvDiffuse_yz_3.xyxx).xy); sampleDiffuse_xyzw_3 = sampleDiffuse_xyzw_2; } else diff --git a/docs/pr-conditional-value-scope.md b/docs/pr-conditional-value-scope.md new file mode 100644 index 0000000..8a73c26 --- /dev/null +++ b/docs/pr-conditional-value-scope.md @@ -0,0 +1,7 @@ +# Preserve values across conditional control flow + +Keep phi copies after a closing conditional and declare branch-written values in their enclosing scope. Regression cases cover reads after a conditional and copies after a nested join. + +Validation: 53 C# tests and all four existing PicaVoxel extraction/decompilation tests pass. C# tests use an en-US default culture; the upstream fr-FR golden-output failure is addressed independently by the numeric-formatting PR. The golden output was regenerated by the emitter for this change only; existing comparison assertions are unchanged. + +This branch is based directly on upstream `1850608`; it does not include the other review branches. No proprietary fixtures or machine-specific paths are added. diff --git a/tests/UnityShaderDecompiler.Tests/ConditionalValueScopeTests.cs b/tests/UnityShaderDecompiler.Tests/ConditionalValueScopeTests.cs new file mode 100644 index 0000000..f985aeb --- /dev/null +++ b/tests/UnityShaderDecompiler.Tests/ConditionalValueScopeTests.cs @@ -0,0 +1,111 @@ +using System; +using System.Linq; +using Parser.Core.Hlsl; +using Parser.Core.Hlsl.Ast; +using Parser.Core.Analysis; +using Parser.Core.Optimizations; +using Parser.DXBC.Instructions; +using Parser.DXBC.IR; +using Xunit; + +namespace UnityShaderDecompiler.Tests; + +public sealed class ConditionalValueScopeTests +{ + [Fact] + public void PhiCopyAfterNestedConditionalRunsAfterBothBranches() + { + IRBlock left = new IRBlock { Id = 0 }; + IRBlock right = new IRBlock { Id = 1 }; + IRBlock join = new IRBlock { Id = 2 }; + join.Statements.Add(new IRStatement.IREndIf()); + join.Predecessors.AddRange(new[] { left, right }); + IRBlock next = new IRBlock { Id = 3 }; + next.Predecessors.Add(join); + IRStatement.IRPhi phi = new IRStatement.IRPhi { Destination = Temp(2) }; + phi.Operands.Add(Temp(1)); + next.Statements.Add(phi); + + IRLeaveSsa.Run(new() { left, right, join, next }); + + Assert.Empty(left.Statements); + Assert.Empty(right.Statements); + Assert.IsType(join.Statements[0]); + Assert.IsType(join.Statements[1]); + } + + [Fact] + public void ValueWrittenInOnlyOneBranchIsDeclaredOutsideTheIf() + { + // A value written in one branch and read after the if must be + // declared outside the branch to avoid HLSL X3004. + IRRegister value = Temp(0); + IRRegister merged = Temp(1); + HlslIfStatement branch = new HlslIfStatement + { + Condition = Float(1), + Then = new HlslBlockStatement(), + Else = new HlslBlockStatement(), + }; + branch.Then.Statements.Add(new HlslAssignmentStatement + { + Destination = merged, + Expression = Float(2), + }); + branch.Else.Statements.Add(new HlslAssignmentStatement + { + Destination = value, + Expression = Float(3), + }); + branch.Else.Statements.Add(new HlslAssignmentStatement + { + Destination = merged, + Expression = new IRExpression.RegisterExpression { Register = value }, + }); + + HlslFunctionNode function = new HlslFunctionNode { Name = "frag", Stage = HlslShaderStage.Fragment }; + function.Statements.Statements.Add(branch); + function.Statements.Statements.Add(new HlslAssignmentStatement + { + Destination = Temp(2), + Expression = new IRExpression.RegisterExpression { Register = value }, + }); + + string source = Print(function); + int declaration = source.IndexOf("float r0_x_1;", StringComparison.Ordinal); + int ifStart = source.IndexOf("if (", StringComparison.Ordinal); + Assert.True(declaration >= 0 && declaration < ifStart, source); + Assert.Contains("r0_x_1 = 3", source); + Assert.DoesNotContain("float r0_x_1 = 3", source); + } + + private static string Print(HlslFunctionNode function) + { + HlslShaderNode shader = new HlslShaderNode { Name = "Test/Scope" }; + HlslSubShaderNode subShader = new HlslSubShaderNode(); + subShader.Passes.Add(new HlslPassNode { FragmentFunction = function }); + shader.SubShaders.Add(subShader); + return HlslPrettyPrinter.Print(shader, fuseTemps: false); + } + + private static IRRegister Temp(uint index, byte mask = 1) + { + IRRegister register = new IRRegister + { + RegisterType = RegisterType.Temp, + Index = index, + Type = IRValueType.Float, + Mask = mask, + ComponentMode = Operand.OperandComponentMode.Mask, + }; + for (int component = 0; component < 4; component++) + if ((mask & (1 << component)) != 0) + register.SsaVersion[component] = 1; + return register; + } + + private static IRExpression.ConstantExpression Float(float value) => new() + { + RawValues = new[] { unchecked((uint)BitConverter.SingleToInt32Bits(value)) }, + }; +} From c14a390a2fa73e21a1164ac3ef599932d77679fa Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 12:17:12 +0100 Subject: [PATCH 07/93] Preserve vector lanes in casts and dot products --- .../Core/Hlsl/HlslMathSimplify.cs | 5 +- .../Core/Hlsl/HlslPrettyPrinter.cs | 26 +++-- Shader Decompiler/Output/HOLO_Holo.shader | 16 +-- docs/pr-vector-expression-widths.md | 7 ++ .../VectorExpressionWidthsTests.cs | 98 +++++++++++++++++++ 5 files changed, 137 insertions(+), 15 deletions(-) create mode 100644 docs/pr-vector-expression-widths.md create mode 100644 tests/UnityShaderDecompiler.Tests/VectorExpressionWidthsTests.cs diff --git a/Shader Decompiler/Core/Hlsl/HlslMathSimplify.cs b/Shader Decompiler/Core/Hlsl/HlslMathSimplify.cs index 88f2270..d15cfe2 100644 --- a/Shader Decompiler/Core/Hlsl/HlslMathSimplify.cs +++ b/Shader Decompiler/Core/Hlsl/HlslMathSimplify.cs @@ -1126,8 +1126,9 @@ private static void SimplifyNormalizes(string[] lines, bool[] drop, { string swz = dotA[(dotIdx + 1)..]; string baseName = dotA[..dotIdx]; - normDims = swz.Distinct().Count(); - vec = baseName; + string lanes = new(swz.Distinct().ToArray()); + normDims = lanes.Length; + vec = $"{baseName}.{lanes}"; } else { diff --git a/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs b/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs index 222df18..9df8003 100644 --- a/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs +++ b/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs @@ -2458,7 +2458,7 @@ IRExpression.BinaryExpression b when IsBitwise(b.Operation) => RenderBitwise(b, IRExpression.BinaryExpression b => $"({RenderExpression(b.Left, ctx)} {BinaryOpText(b.Operation)} {RenderExpression(b.Right, ctx)})", IRExpression.UnaryExpression u => RenderUnary(u, ctx), IRExpression.ConditionalExpression c => $"({RenderExpression(c.Condition, ctx)} ? {RenderExpression(c.TrueExpression, ctx)} : {RenderExpression(c.FalseExpression, ctx)})", - IRExpression.DotProductExpression d => $"dot({RenderExpression(d.Left, ctx)}, {RenderExpression(d.Right, ctx)})", + IRExpression.DotProductExpression d => RenderDotProduct(d, ctx), IRExpression.SwizzleExpression s => RenderSwizzle(s, ctx), IRExpression.RegisterExpression r => RenderRegisterRead(r.Register, ctx), IRExpression.ConstantExpression => expr.ToString()!, @@ -2509,6 +2509,14 @@ private static bool IsLeading(List components) return true; } + private static string RenderDotProduct(IRExpression.DotProductExpression dot, PrintContext ctx) + { + if (dot.Components is < 2 or > 4) + throw new InvalidOperationException($"Unsupported dot-product width: {dot.Components}"); + string lanes = "xyzw"[..dot.Components]; + return $"dot(({RenderExpression(dot.Left, ctx)}).{lanes}, ({RenderExpression(dot.Right, ctx)}).{lanes})"; + } + private static string RenderUnary(IRExpression.UnaryExpression u, PrintContext ctx) => u.Operation switch { IRExpression.UnaryExpression.UnaryOperation.Negate => $"-{RenderExpression(u.Operand, ctx)}", @@ -2594,11 +2602,17 @@ private static string RenderIntrinsic(IRExpression.IntrinsicExpression expr, Pri // Casts use C-style cast syntax, not a function call. switch (expr.Intrinsic) { - case IRExpression.IRIntrinsic.CastFloat: return $"(float)({Arg(0)})"; - case IRExpression.IRIntrinsic.CastInt: return $"(int)({Arg(0)})"; - case IRExpression.IRIntrinsic.CastUInt: return $"(uint)({Arg(0)})"; - case IRExpression.IRIntrinsic.CastDouble: return $"(double)({Arg(0)})"; - case IRExpression.IRIntrinsic.CastBool: return $"(bool)({Arg(0)})"; + case IRExpression.IRIntrinsic.CastFloat: return $"({CastType("float")})({Arg(0)})"; + case IRExpression.IRIntrinsic.CastInt: return $"({CastType("int")})({Arg(0)})"; + case IRExpression.IRIntrinsic.CastUInt: return $"({CastType("uint")})({Arg(0)})"; + case IRExpression.IRIntrinsic.CastDouble: return $"({CastType("double")})({Arg(0)})"; + case IRExpression.IRIntrinsic.CastBool: return $"({CastType("bool")})({Arg(0)})"; + } + + string CastType(string scalar) + { + int width = ExpressionWidth(expr.Arguments[0], ctx); + return width <= 1 ? scalar : $"{scalar}{width}"; } string? name = expr.Intrinsic switch diff --git a/Shader Decompiler/Output/HOLO_Holo.shader b/Shader Decompiler/Output/HOLO_Holo.shader index 95947ba..c655ca6 100644 --- a/Shader Decompiler/Output/HOLO_Holo.shader +++ b/Shader Decompiler/Output/HOLO_Holo.shader @@ -217,11 +217,13 @@ Shader "HOLO/Holo" float3 viewNormal_xyz_5 = (mad(objectToView[0].xyz.xyzx, i.normal0.xxxx, ((objectToView[1].xyz.xyzx * i.normal0.yyyy)).xyzx)).xyz; float3 viewNormal_xyz_6 = (mad(objectToView[2].xyz.xyzx, i.normal0.zzzz, viewNormal_xyz_5.xyzx)).xyz; float3 viewNormal_xyz_7 = (mad(objectToView[3].xyz.xyzx, i.normal0.wwww, viewNormal_xyz_6.xyzx)).xyz; - float2 unitViewNormal_xy_8 = normalize(viewNormal_xyz_7); - float worldNormal_x_6 = dot(i.normal0.xyzx, unity_WorldToObject[0].xyzx); - float worldNormal_y_6 = dot(i.normal0.xyzx, unity_WorldToObject[1].xyzx); - float worldNormal_z_6 = dot(i.normal0.xyzx, unity_WorldToObject[2].xyzx); - float r0_z_10 = dot(float4(worldNormal_x_6, worldNormal_y_6, worldNormal_z_6, worldNormal_x_6), float4(worldNormal_x_6, worldNormal_y_6, worldNormal_z_6, worldNormal_x_6)); + float r0_z_8 = dot(viewNormal_xyz_7.xyz, viewNormal_xyz_7.xyz); + float r0_z_9 = rsqrt(r0_z_8); + float2 unitViewNormal_xy_8 = ((r0_z_9.xxxx * viewNormal_xyz_7.xyxx)).xy; + float worldNormal_x_6 = dot(i.normal0.xyz, unity_WorldToObject[0].xyz); + float worldNormal_y_6 = dot(i.normal0.xyz, unity_WorldToObject[1].xyz); + float worldNormal_z_6 = dot(i.normal0.xyz, unity_WorldToObject[2].xyz); + float r0_z_10 = dot((float4(worldNormal_x_6, worldNormal_y_6, worldNormal_z_6, worldNormal_x_6)).xyz, (float4(worldNormal_x_6, worldNormal_y_6, worldNormal_z_6, worldNormal_x_6)).xyz); float r0_z_11 = rsqrt(r0_z_10); float3 unitWorldNormal_xyz_7 = ((r0_z_11.xxxx * float4(worldNormal_x_6, worldNormal_y_6, worldNormal_z_6, worldNormal_x_6))).xyz; o.texcoord2.xy = (mad(unitViewNormal_xy_8.xyxx, float4(0.5, 0.5, 0, 0), float4(0.5, 0.5, 0, 0))).xy; @@ -229,13 +231,13 @@ Shader "HOLO/Holo" float dirToSurface_x_9 = dirToSurface_xyzw_9.x; float dirToSurface_y_9 = dirToSurface_xyzw_9.y; float dirToSurface_z_12 = dirToSurface_xyzw_9.z; - float r0_w_6 = dot(float4(dirToSurface_x_9, dirToSurface_y_9, dirToSurface_z_12, dirToSurface_x_9), float4(dirToSurface_x_9, dirToSurface_y_9, dirToSurface_z_12, dirToSurface_x_9)); + float r0_w_6 = dot((float4(dirToSurface_x_9, dirToSurface_y_9, dirToSurface_z_12, dirToSurface_x_9)).xyz, (float4(dirToSurface_x_9, dirToSurface_y_9, dirToSurface_z_12, dirToSurface_x_9)).xyz); float r0_w_7 = rsqrt(r0_w_6); float4 unitDirToSurface_xyzw_10 = (r0_w_7.xxxx * float4(dirToSurface_x_9, dirToSurface_y_9, dirToSurface_z_12, dirToSurface_x_9)); float unitDirToSurface_x_10 = unitDirToSurface_xyzw_10.x; float unitDirToSurface_y_10 = unitDirToSurface_xyzw_10.y; float unitDirToSurface_z_13 = unitDirToSurface_xyzw_10.z; - float nDotV_x_11 = dot(float4(unitDirToSurface_x_10, unitDirToSurface_y_10, unitDirToSurface_z_13, unitDirToSurface_x_10), unitWorldNormal_xyz_7.xyzx); + float nDotV_x_11 = dot((float4(unitDirToSurface_x_10, unitDirToSurface_y_10, unitDirToSurface_z_13, unitDirToSurface_x_10)).xyz, unitWorldNormal_xyz_7.xyz); float r0_y_11 = (_Scale * _Bias); float fresnel_x_15 = pow((nDotV_x_11 + _t), _Power); o.texcoord4.x = (fresnel_x_15 * r0_y_11); diff --git a/docs/pr-vector-expression-widths.md b/docs/pr-vector-expression-widths.md new file mode 100644 index 0000000..9363f17 --- /dev/null +++ b/docs/pr-vector-expression-widths.md @@ -0,0 +1,7 @@ +# Preserve vector lanes in casts and dot products + +Use the operand width for numeric casts and the instruction width for dot products. Keep the selected lanes when recognizing normalization; a three-lane expression must not read the fourth lane. + +Validation: 54 C# tests and all four existing PicaVoxel extraction/decompilation tests pass. C# tests use an en-US default culture; the upstream fr-FR golden-output failure is addressed independently by the numeric-formatting PR. The golden output was regenerated by the emitter for this change only; existing comparison assertions are unchanged. + +This branch is based directly on upstream `1850608`; it does not include the other review branches. No proprietary fixtures or machine-specific paths are added. diff --git a/tests/UnityShaderDecompiler.Tests/VectorExpressionWidthsTests.cs b/tests/UnityShaderDecompiler.Tests/VectorExpressionWidthsTests.cs new file mode 100644 index 0000000..9ce14a1 --- /dev/null +++ b/tests/UnityShaderDecompiler.Tests/VectorExpressionWidthsTests.cs @@ -0,0 +1,98 @@ +using System; +using System.Linq; +using Parser.Core.Hlsl; +using Parser.Core.Hlsl.Ast; +using Parser.Core.Analysis; +using Parser.Core.Optimizations; +using Parser.DXBC.Instructions; +using Parser.DXBC.IR; +using Xunit; + +namespace UnityShaderDecompiler.Tests; + +public sealed class VectorExpressionWidthsTests +{ + [Fact] + public void NormalizeSimplificationKeepsTheDotProductLanes() + { + string body = "float lengthSquared = dot(cb1_values[5].xyz, cb1_values[5].xyz);\n" + + "float inverseLength = rsqrt(lengthSquared);\n" + + "float3 result = ((inverseLength.xxxx * cb1_values[5].xyzx)).xyz;\n"; + + string simplified = HlslMathSimplify.Simplify(body); + Assert.Matches(@"normalize\(cb1_values\s*\[5\]\.xyz\)", simplified); + Assert.DoesNotMatch(@"normalize\(cb1_values\s*\[5\]\)", simplified); + } + + [Fact] + public void ThreeComponentDotDoesNotReadTheFourthLane() + { + HlslFunctionNode function = new HlslFunctionNode { Name = "frag", Stage = HlslShaderStage.Fragment }; + function.Statements.Statements.Add(new HlslAssignmentStatement + { + Destination = Temp(2), + Expression = new IRExpression.DotProductExpression + { + Components = 3, + Left = new IRExpression.RegisterExpression { Register = Temp(0, 15) }, + Right = new IRExpression.RegisterExpression { Register = Temp(1, 15) }, + }, + }); + + string source = Print(function); + Assert.Matches(@"dot\(\(.+\)\.xyz, \(.+\)\.xyz\)", source); + } + + [Fact] + public void VectorCastPreservesWidthForComponentAccess() + { + // A scalar cast followed by vector component access causes HLSL + // X3018 (invalid subscript). + IRRegister sourceRegister = Temp(0, 15); + IRExpression.IntrinsicExpression cast = new IRExpression.IntrinsicExpression + { + Intrinsic = IRExpression.IRIntrinsic.CastFloat, + }; + cast.Arguments.Add(new IRExpression.RegisterExpression { Register = sourceRegister }); + HlslFunctionNode function = new HlslFunctionNode { Name = "frag", Stage = HlslShaderStage.Fragment }; + function.Statements.Statements.Add(new HlslAssignmentStatement + { + Destination = Temp(1, 15), + Expression = cast, + }); + + string source = Print(function); + Assert.Contains("(float4)", source); + Assert.DoesNotContain("(float)r0_xyzw_1", source); + } + + private static string Print(HlslFunctionNode function) + { + HlslShaderNode shader = new HlslShaderNode { Name = "Test/Scope" }; + HlslSubShaderNode subShader = new HlslSubShaderNode(); + subShader.Passes.Add(new HlslPassNode { FragmentFunction = function }); + shader.SubShaders.Add(subShader); + return HlslPrettyPrinter.Print(shader, fuseTemps: false); + } + + private static IRRegister Temp(uint index, byte mask = 1) + { + IRRegister register = new IRRegister + { + RegisterType = RegisterType.Temp, + Index = index, + Type = IRValueType.Float, + Mask = mask, + ComponentMode = Operand.OperandComponentMode.Mask, + }; + for (int component = 0; component < 4; component++) + if ((mask & (1 << component)) != 0) + register.SsaVersion[component] = 1; + return register; + } + + private static IRExpression.ConstantExpression Float(float value) => new() + { + RawValues = new[] { unchecked((uint)BitConverter.SingleToInt32Bits(value)) }, + }; +} From 3f9942aa5325b93f2b0200d50985cb337a524b4a Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 12:19:04 +0100 Subject: [PATCH 08/93] Preserve resource bindings and raw constant-buffer layout --- .../Core/Hlsl/HlslPrettyPrinter.cs | 24 ++++- .../DXBC/Metadata/Shadermetadata.cs | 4 + Shader Decompiler/Decompiler.cs | 34 ++++++- Shader Decompiler/Output/HOLO_Holo.shader | 81 +++++++---------- docs/pr-resource-binding-layouts.md | 7 ++ .../CbufferEmissionTests.cs | 58 ++++++++++++ .../ResourceBindingLayoutsTests.cs | 88 +++++++++++++++++++ .../SamplerBindingTests.cs | 22 +++++ 8 files changed, 261 insertions(+), 57 deletions(-) create mode 100644 docs/pr-resource-binding-layouts.md create mode 100644 tests/UnityShaderDecompiler.Tests/CbufferEmissionTests.cs create mode 100644 tests/UnityShaderDecompiler.Tests/ResourceBindingLayoutsTests.cs create mode 100644 tests/UnityShaderDecompiler.Tests/SamplerBindingTests.cs diff --git a/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs b/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs index 222df18..1742bd4 100644 --- a/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs +++ b/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs @@ -354,6 +354,14 @@ bool IsMatrixName(string name) => private static void PrintResource(StringBuilder sb, HlslResourceNode res, bool stripRegisterBindings, Dictionary? cbufferRenames = null) { + if (res.Kind == HlslResourceKind.ConstantBuffer + && res.Variables.FirstOrDefault(v => v.Name == $"cb{res.Slot}_values" && v.ArraySize is not null) is { } raw) + { + sb.Append(" cbuffer ").Append(CbufferName(res)).Append(" : register(b").Append(res.Slot) + .Append(") { float4 ").Append(raw.Name).Append('[').Append(raw.ArraySize).Append("]; };\n"); + return; + } + // Unity standard cbuffers are assigned to per-stage slots by Unity's // compiler from the cbuffer NAME alone — an explicit register binding // would conflict when vertex and fragment map the same name to @@ -375,9 +383,7 @@ private static void PrintResource(StringBuilder sb, HlslResourceNode res, bool s // same name causes "Duplicate constant buffer declaration". // Rename them while keeping ALL variables and the register // binding so the bytecode still reads from the correct slot. - string cbufferName = IsUnityCbuffer(res.Name) - ? $"_{res.Name.TrimStart('_')}CB_b{res.Slot}" - : res.Name; + string cbufferName = CbufferName(res); sb.Append(" cbuffer ").Append(cbufferName); if (!string.IsNullOrEmpty(reg)) @@ -540,6 +546,11 @@ private static void PrintResource(StringBuilder sb, HlslResourceNode res, bool s private static bool IsUnityCbuffer(string name) => name.StartsWith("Unity", StringComparison.Ordinal) || name == "_Globals"; + // A partial member layout does not invalidate the recovered buffer identity. + private static string CbufferName(HlslResourceNode resource) => IsUnityCbuffer(resource.Name) + ? $"_{resource.Name.TrimStart('_')}CB_b{resource.Slot}" + : resource.Name; + // ---------- Stage 8: Structs ---------- private static void PrintStruct(StringBuilder sb, HlslStructNode s) @@ -564,6 +575,8 @@ private sealed class PrintContext // used to render RDEF-less cbN[slot] reads as real variable names. public Dictionary Cbuffers { get; set; } = new(); + public HashSet RawCbufferSlots { get; } = new(); + // Identifier each defining instruction actually declared for a // value, keyed by the value's structural identity (storage location // + SSA version), NOT by its rendered name string. Reads resolve @@ -657,6 +670,9 @@ private static void PrintFunction( if (resources is not null) foreach (HlslResourceNode res in resources) { + if (res.Kind == HlslResourceKind.ConstantBuffer + && res.Variables.Any(v => v.Name == $"cb{res.Slot}_values" && v.ArraySize is not null)) + ctx.RawCbufferSlots.Add((int)res.Slot); if (res.Kind == HlslResourceKind.Texture) { ctx.TextureTypeBySlot[(int)res.Slot] = res.TypeHint ?? "Texture2D"; @@ -1651,6 +1667,8 @@ private static string CbufferElementName(IRRegister reg, PrintContext ctx) // 3. synthesized cbN_values[slot] fallback array. private static string RenderCbufferRead(IRRegister reg, PrintContext ctx) { + if (reg.Indices.Count >= 2 && ctx.RawCbufferSlots.Contains((int)reg.Indices[0])) + return CbufferElementName(reg, ctx) + MaskOrSwizzleSuffix(reg); if (reg.SymbolicName is not null) { string name = reg.SymbolicName; diff --git a/Shader Decompiler/DXBC/Metadata/Shadermetadata.cs b/Shader Decompiler/DXBC/Metadata/Shadermetadata.cs index 63bc48b..e00d716 100644 --- a/Shader Decompiler/DXBC/Metadata/Shadermetadata.cs +++ b/Shader Decompiler/DXBC/Metadata/Shadermetadata.cs @@ -197,6 +197,7 @@ public static PassMetadata From(JsonElement el) public sealed class CbufferMetadata { public int Slot { get; init; } = -1; + public int Size { get; init; } // Stage this buffer is bound in (Vertex/Fragment/Geometry/...). Stages // bind their own cbuffer tables, so two different buffers can legally // share a slot number across stages (e.g. UnityPerDraw and $Globals @@ -212,6 +213,9 @@ public static CbufferMetadata From(JsonElement el) Slot = el.TryGetProperty("slot", out var s) && s.ValueKind == JsonValueKind.Number ? s.GetInt32() : -1, + Size = el.TryGetProperty("size", out JsonElement size) && size.ValueKind == JsonValueKind.Number + ? size.GetInt32() + : 0, Stage = el.TryGetProperty("stage", out var st) && st.ValueKind == JsonValueKind.String ? st.GetString() ?? "" : "", diff --git a/Shader Decompiler/Decompiler.cs b/Shader Decompiler/Decompiler.cs index 5a44dad..b9bfcdc 100644 --- a/Shader Decompiler/Decompiler.cs +++ b/Shader Decompiler/Decompiler.cs @@ -612,10 +612,9 @@ private static void AttachPassProgram( // Cbuffers merge by slot: vert and frag subprograms of the same // pass touch the same constant-buffer slots, so union the // members (keeping the largest array size for any fallback). + // Equal names do not make resources at different bindings identical. HlslResourceNode? existing = pass.Resources.FirstOrDefault(r => - r.Kind == res.Kind && (res.Kind == HlslResourceKind.ConstantBuffer - ? r.Slot == res.Slot - : r.Slot == res.Slot || r.Name == res.Name)); + r.Kind == res.Kind && r.Slot == res.Slot); if (existing is null) { @@ -652,6 +651,35 @@ private static void AttachPassProgram( } } + // A fallback array is shared by all stages in the emitted HLSL pass. + // Once present, every stage reads raw slots, including stages whose + // reads were resolved to named metadata members before the merge. + foreach (HlslResourceNode buffer in pass.Resources.Where(r => r.Kind == HlslResourceKind.ConstantBuffer)) + { + HlslCBufferVariable? raw = buffer.Variables.FirstOrDefault( + v => v.Name == $"cb{buffer.Slot}_values" && v.ArraySize is not null); + if (raw is null) + continue; + int metadataRegisters = pass.Cbuffers + .Where(entry => entry.Key.Slot == (int)buffer.Slot) + .Select(entry => (entry.Value.Size + 15) / 16) + .DefaultIfEmpty(0) + .Max(); + int count = Math.Max(raw.ArraySize!.Value, metadataRegisters); + if (count > raw.ArraySize.Value) + { + buffer.Variables.Remove(raw); + buffer.Variables.Add(new HlslCBufferVariable + { + Name = raw.Name, + TypeName = raw.TypeName, + Offset = raw.Offset, + Size = (uint)count * 16, + ArraySize = count, + }); + } + } + if (c.Function.InputStruct is not null) pass.Structs.Add(c.Function.InputStruct); if (c.Function.OutputStruct is not null) pass.Structs.Add(c.Function.OutputStruct); diff --git a/Shader Decompiler/Output/HOLO_Holo.shader b/Shader Decompiler/Output/HOLO_Holo.shader index 95947ba..bc3cda7 100644 --- a/Shader Decompiler/Output/HOLO_Holo.shader +++ b/Shader Decompiler/Output/HOLO_Holo.shader @@ -45,29 +45,7 @@ Shader "HOLO/Holo" ZWrite On Blend SrcAlpha OneMinusSrcColor HLSLPROGRAM - cbuffer _GlobalsCB_b0 - { - float4 _Color; - float4 _diff_Color; - float4 _Diffuse_ST; - float4 _N_map_ST; - float4 _M_map_ST; - float _intensity; - float _deform; - float _Bias; - float _Scale; - float _Power; - float _Speed; - float _t; - float _X; - float _Y; - float _glitchColor; - float _monochrom; - float _Opacity; - float _noise_details; - float _OriginalUVSwitch; - float4 cb0_values[8]; - }; + cbuffer _GlobalsCB_b0 : register(b0) { float4 cb0_values[12]; }; cbuffer _UnityPerCameraCB_b1 { float4 _Time : packoffset(c0); @@ -86,6 +64,7 @@ Shader "HOLO/Holo" SamplerState sampler_MainTex; Texture2D _MainTex; SamplerState sampler_Diffuse; + SamplerState sampler_MainTex2; SamplerState sampler_originalDiffuse; Texture2D _originalDiffuse; Texture2D _Diffuse; @@ -127,16 +106,16 @@ Shader "HOLO/Holo" { program1Output o = (program1Output)0; float4x4 objectToView = mul(unity_ObjectToWorld, unity_MatrixV); - float r0_w_1 = (_Speed * _Time.x); + float r0_w_1 = (cb0_values[8].y * _Time.x); float r0_w_2 = (r0_w_1 * 60); float r0_w_3 = (sin(r0_w_2) + 1); - float r1_w_1 = (cos(r0_w_2) * _M_map_ST.y); - float r0_w_4 = (r0_w_3 * _M_map_ST.w); + float r1_w_1 = (cos(r0_w_2) * cb0_values[6].y); + float r0_w_4 = (r0_w_3 * cb0_values[6].w); float4 uvMMap_xyzw_2 = mad(i.texcoord0.xyxx, r1_w_1.xxxx, r0_w_4.xxxx); float uvMMap_x_2 = uvMMap_xyzw_2.x; float uvMMap_y_1 = uvMMap_xyzw_2.y; float4 sampleMMap_xyzw_3 = _MainTex.SampleLevel(sampler_MainTex, (float4(uvMMap_x_2, uvMMap_y_1, uvMMap_x_2, uvMMap_x_2)).xy, 0); - float r0_w_5 = (int)(_noise_details); + float r0_w_5 = (int)(cb0_values[10].x); float4 r4_xyzw_3 = (_Time.xxyz * float4(0, 0.1, 0.1, 0.1)); float r4_y_3 = r4_xyzw_3.y; float r4_z_2 = r4_xyzw_3.z; @@ -188,8 +167,8 @@ Shader "HOLO/Holo" float4 r4_xyzw_6 = (sampleMMap_xyzw_3.xxxx * float4(r4_y_5, r4_z_4, r4_w_3, r4_y_5)); float r4_y_6 = r4_xyzw_6.y; float r4_z_5 = r4_xyzw_6.z; - float r4_x_5 = (r4_x_4 * _X); - float r4_y_7 = (r4_y_6 * _Y); + float r4_x_5 = (r4_x_4 * cb0_values[8].w); + float r4_y_7 = (r4_y_6 * cb0_values[9].x); float r4_x_6 = ((float4(r4_x_5, r4_y_7, r4_z_5, r4_x_5) * i.normal0.xyzx)).x; float4 r4_xyzw_8 = (float4(r4_x_5, r4_y_7, r4_z_5, r4_x_5) * i.normal0.xyzx); float r4_y_8 = r4_xyzw_8.y; @@ -236,8 +215,8 @@ Shader "HOLO/Holo" float unitDirToSurface_y_10 = unitDirToSurface_xyzw_10.y; float unitDirToSurface_z_13 = unitDirToSurface_xyzw_10.z; float nDotV_x_11 = dot(float4(unitDirToSurface_x_10, unitDirToSurface_y_10, unitDirToSurface_z_13, unitDirToSurface_x_10), unitWorldNormal_xyz_7.xyzx); - float r0_y_11 = (_Scale * _Bias); - float fresnel_x_15 = pow((nDotV_x_11 + _t), _Power); + float r0_y_11 = (cb0_values[7].w * cb0_values[7].z); + float fresnel_x_15 = pow((nDotV_x_11 + cb0_values[8].z), cb0_values[8].x); o.texcoord4.x = (fresnel_x_15 * r0_y_11); o.texcoord0.xyzw = i.texcoord0.xyzw; o.texcoord1.xyzw = float4(0, 0, 0, 0); @@ -251,28 +230,28 @@ Shader "HOLO/Holo" program3Output frag(program3Input i) { program3Output o = (program3Output)0; - float4 r0_xyzw_2 = (((_Speed * _Time.x)).xxxx * float4(30, 60, 120, 0)); + float4 r0_xyzw_2 = (((cb0_values[8].y * _Time.x)).xxxx * float4(30, 60, 120, 0)); float r0_x_2 = r0_xyzw_2.x; float r0_y_1 = r0_xyzw_2.y; float r0_z_1 = r0_xyzw_2.z; float r1_x_1 = sin(r0_y_1); float r0_y_2 = (r1_x_1 + 1); - float2 r1_yz_1 = ((r0_x_2.xxxx * _N_map_ST.zzwz)).yz; - float2 uvNMap_yz_2 = (mad(i.texcoord0.xxyx, _N_map_ST.xxyx, r1_yz_1.xxyx)).yz; - float4 sampleNMap_xyzw_1 = _MainTex.Sample(s2, (uvNMap_yz_2.xyxx).xy); - float r0_w_1 = (cos(r0_y_1) * _M_map_ST.y); - float r0_y_3 = (r0_y_2 * _M_map_ST.w); + float2 r1_yz_1 = ((r0_x_2.xxxx * cb0_values[5].zzwz)).yz; + float2 uvNMap_yz_2 = (mad(i.texcoord0.xxyx, cb0_values[5].xxyx, r1_yz_1.xxyx)).yz; + float4 sampleNMap_xyzw_1 = _MainTex.Sample(sampler_MainTex2, (uvNMap_yz_2.xyxx).xy); + float r0_w_1 = (cos(r0_y_1) * cb0_values[6].y); + float r0_y_3 = (r0_y_2 * cb0_values[6].w); float4 uvMMap_xyzw_4 = mad(i.texcoord0.xxxy, r0_w_1.xxxx, r0_y_3.xxxx); float uvMMap_y_4 = uvMMap_xyzw_4.y; float uvMMap_w_2 = uvMMap_xyzw_4.w; float4 sampleMMap_xyzw_2 = _originalDiffuse.Sample(sampler_originalDiffuse, (float4(uvMMap_y_4, uvMMap_w_2, uvMMap_y_4, uvMMap_y_4)).xy); - float4 r0_xyzw_5 = (_OriginalUVSwitch.xxxx == float4(0, 0, 0, 1)); + float4 r0_xyzw_5 = (cb0_values[11].yyyy == float4(0, 0, 0, 1)); float r0_y_5 = r0_xyzw_5.y; float r0_w_3 = r0_xyzw_5.w; float4 sampleDiffuse_xyzw_3; if (r0_y_5) { - float2 uvDiffuse_yz_3 = (mad(i.texcoord2.xxyx, _Diffuse_ST.yyyy, _Diffuse_ST.wwww)).yz; + float2 uvDiffuse_yz_3 = (mad(i.texcoord2.xxyx, cb0_values[4].yyyy, cb0_values[4].wwww)).yz; float4 sampleDiffuse_xyzw_2 = _Diffuse.Sample(sampler_Diffuse, (uvDiffuse_yz_3.xyxx).xy); sampleDiffuse_xyzw_3 = sampleDiffuse_xyzw_2; } @@ -294,8 +273,8 @@ Shader "HOLO/Holo" float r0_x_4 = r0_xyzw_4.x; float r0_z_3 = r0_xyzw_4.z; float r0_w_4 = (r0_x_4 * i.texcoord0.x); - float2 r1_yz_5 = (mad(r0_w_4.xxxx, _intensity.xxxx, i.texcoord0.xxyx)).yz; - float r0_w_5 = (int)(_noise_details); + float2 r1_yz_5 = (mad(r0_w_4.xxxx, cb0_values[7].xxxx, i.texcoord0.xxyx)).yz; + float r0_w_5 = (int)(cb0_values[10].x); float4 r2_xyzw_3 = (_Time.xyxx * float4(0.1, 0.1, 0, 0)); float r2_x_3 = r2_xyzw_3.x; float r2_y_2 = r2_xyzw_3.y; @@ -335,30 +314,30 @@ Shader "HOLO/Holo" r2_zw_3 = r2_zw_4.xy; noiseAccum_xy_3 = noiseAccum_xy_4; } - float2 r1_yz_6 = ((noiseAccum_xy_3.xxyx * _glitchColor.xxxx)).yz; + float2 r1_yz_6 = ((noiseAccum_xy_3.xxyx * cb0_values[9].yyyy)).yz; float r0_x_5 = ((float4(r0_x_4, r0_y_6, r0_z_3, r0_x_4) * float4(r1_yz_6.x, r1_x_1, r1_yz_6.x, r1_yz_6.x))).x; float4 r0_xyzw_7 = (float4(r0_x_4, r0_y_6, r0_z_3, r0_x_4) * float4(r1_yz_6.x, r1_x_1, r1_yz_6.x, r1_yz_6.x)); float r0_y_7 = r0_xyzw_7.y; float r0_z_4 = r0_xyzw_7.z; float r0_y_8 = (r1_yz_6.y * r0_y_7); - float4 r0_xyzw_6 = (float4(r0_x_5, r0_y_8, r0_z_4, r0_x_5) * _intensity.xxxx); + float4 r0_xyzw_6 = (float4(r0_x_5, r0_y_8, r0_z_4, r0_x_5) * cb0_values[7].xxxx); float r0_x_6 = r0_xyzw_6.x; float r0_y_9 = r0_xyzw_6.y; float r0_z_5 = r0_xyzw_6.z; - float r1_y_7 = mad(r0_y_9, 0.375, _Color.y); - float4 r1_xyzw_2 = mad(float4(r0_x_6, r0_x_6, r0_z_5, r0_x_6), float4(0.1875, 0, 0.75, 0), _Color.xxzx); + float r1_y_7 = mad(r0_y_9, 0.375, cb0_values[2].y); + float4 r1_xyzw_2 = mad(float4(r0_x_6, r0_x_6, r0_z_5, r0_x_6), float4(0.1875, 0, 0.75, 0), cb0_values[2].xxzx); float r1_x_2 = r1_xyzw_2.x; float r1_z_7 = r1_xyzw_2.z; - float r4_w_6 = (sampleDiffuse_xyzw_5.w * _Opacity); - float r0_x_7 = ((float4(sampleDiffuse_xyzw_5.x, sampleDiffuse_xyzw_5.y, sampleDiffuse_xyzw_5.z, r4_w_6) * _diff_Color)).x; - float4 r0_xyzw_10 = (float4(sampleDiffuse_xyzw_5.x, sampleDiffuse_xyzw_5.y, sampleDiffuse_xyzw_5.z, r4_w_6) * _diff_Color); + float r4_w_6 = (sampleDiffuse_xyzw_5.w * cb0_values[9].w); + float r0_x_7 = ((float4(sampleDiffuse_xyzw_5.x, sampleDiffuse_xyzw_5.y, sampleDiffuse_xyzw_5.z, r4_w_6) * cb0_values[3].xyzw)).x; + float4 r0_xyzw_10 = (float4(sampleDiffuse_xyzw_5.x, sampleDiffuse_xyzw_5.y, sampleDiffuse_xyzw_5.z, r4_w_6) * cb0_values[3].xyzw); float r0_y_10 = r0_xyzw_10.y; float r0_z_6 = r0_xyzw_10.z; float r0_w_6 = r0_xyzw_10.w; - float r1_w_3 = (_monochrom == 1); + float r1_w_3 = (cb0_values[9].z == 1); float r2_x_5 = (r1_z_7 + (r1_y_7 + r1_x_2)); float r2_y_3 = (r0_y_10 + r0_x_7); - float r2_y_4 = mad(sampleDiffuse_xyzw_5.z, _diff_Color.z, r2_y_3); + float r2_y_4 = mad(sampleDiffuse_xyzw_5.z, cb0_values[3].z, r2_y_3); float4 r2_xyzw_6 = (float4(r2_x_5, r2_y_4, r2_x_5, r2_x_5) * float4(0.33333334, 0.33333334, 0, 0)); float r2_x_6 = r2_xyzw_6.x; float r2_y_5 = r2_xyzw_6.y; @@ -374,7 +353,7 @@ Shader "HOLO/Holo" float r1_x_4 = r1_xyzw_4.x; float r1_y_9 = r1_xyzw_4.y; float r1_z_9 = r1_xyzw_4.z; - float r1_w_4 = (-r0_w_6 + _Color.w); + float r1_w_4 = (-r0_w_6 + cb0_values[2].w); o.sv_Target0.xyzw = mad(i.texcoord4.xxxx, float4(r1_x_4, r1_y_9, r1_z_9, r1_w_4), float4(monochromSelect_x_8, monochromSelect_y_11, monochromSelect_z_7, r0_w_6)); return o; } diff --git a/docs/pr-resource-binding-layouts.md b/docs/pr-resource-binding-layouts.md new file mode 100644 index 0000000..1db0a51 --- /dev/null +++ b/docs/pr-resource-binding-layouts.md @@ -0,0 +1,7 @@ +# Preserve resource bindings and raw constant-buffer layout + +Keep resources with different slots distinct even when their recovered names coincide. When a buffer needs raw fallback storage, emit one array starting at byte zero while retaining its name, binding and declared size. Native reflection tests verify the physical buffer layout. + +Validation: 55 C# tests and all four existing PicaVoxel extraction/decompilation tests pass. C# tests use an en-US default culture; the upstream fr-FR golden-output failure is addressed independently by the numeric-formatting PR. The golden output was regenerated by the emitter for this change only; existing comparison assertions are unchanged. + +This branch is based directly on upstream `1850608`; it does not include the other review branches. No proprietary fixtures or machine-specific paths are added. diff --git a/tests/UnityShaderDecompiler.Tests/CbufferEmissionTests.cs b/tests/UnityShaderDecompiler.Tests/CbufferEmissionTests.cs new file mode 100644 index 0000000..f91a00a --- /dev/null +++ b/tests/UnityShaderDecompiler.Tests/CbufferEmissionTests.cs @@ -0,0 +1,58 @@ +using System; +using System.Linq; +using System.Text.RegularExpressions; +using Parser; +using Parser.Core.Hlsl.Ast; +using Parser.DXBC; +using Parser.DXBC.Chunks; + +namespace UnityShaderDecompiler.Tests; + +public class CbufferEmissionTests +{ + [Theory] + [InlineData("MaterialParameters", "MaterialParameters")] + [InlineData("_Globals", "_GlobalsCB_b1")] + [Trait("Category", "D3D")] + public void PartialLayoutPreservesBufferIdentityAndRawByteOffsets(string name, string emittedName) + { + if (!OperatingSystem.IsWindows()) return; + + HlslShaderNode shader = new() { Name = "Test/PartialLayout" }; + HlslSubShaderNode subShader = new(); + HlslPassNode pass = new() + { + FragmentFunction = new HlslFunctionNode { Name = "frag", Stage = HlslShaderStage.Fragment }, + }; + HlslResourceNode buffer = new() + { + Name = name, Kind = HlslResourceKind.ConstantBuffer, Slot = 1, + }; + buffer.Variables.Add(new HlslCBufferVariable + { + Name = "KnownMember", TypeName = "float4", Offset = 16, Size = 16, + }); + buffer.Variables.Add(new HlslCBufferVariable + { + Name = "cb1_values", TypeName = "float4", Offset = 0, Size = 32, ArraySize = 2, + }); + pass.Resources.Add(buffer); + subShader.Passes.Add(pass); + shader.SubShaders.Add(subShader); + string text = HlslPrettyPrinter.Print(shader, fuseTemps: false); + string declaration = Regex.Match(text, @"cbuffer\b.*?};", RegexOptions.Singleline).Value; + Assert.NotEmpty(declaration); + string source = declaration + "\nfloat4 main() : SV_Target { return cb1_values[1]; }"; + DxbcFile compiled = new(); + compiled.Load(RecompileVerify.CompileHlsl(source, "main", "ps_5_0")); + + RdefConstantBuffer reflected = Assert.Single(compiled.ResourceDefinition!.ConstantBuffers); + Assert.Equal(emittedName, reflected.Name); + RdefVariable storage = Assert.Single(reflected.Variables); + Assert.Equal("cb1_values", storage.Name); + Assert.Equal(0u, storage.Offset); + Assert.Equal(32u, storage.Size); + Assert.Equal(32u, reflected.Size); + Assert.Equal(1u, compiled.ResourceDefinition.ResourceBindings.Single(b => b.Name == emittedName).BindPoint); + } +} diff --git a/tests/UnityShaderDecompiler.Tests/ResourceBindingLayoutsTests.cs b/tests/UnityShaderDecompiler.Tests/ResourceBindingLayoutsTests.cs new file mode 100644 index 0000000..cc763f6 --- /dev/null +++ b/tests/UnityShaderDecompiler.Tests/ResourceBindingLayoutsTests.cs @@ -0,0 +1,88 @@ +using System; +using System.Linq; +using Parser.Core.Hlsl; +using Parser.Core.Hlsl.Ast; +using Parser.Core.Analysis; +using Parser.Core.Optimizations; +using Parser.DXBC.Instructions; +using Parser.DXBC.IR; +using Xunit; + +namespace UnityShaderDecompiler.Tests; + +public sealed class ResourceBindingLayoutsTests +{ + [Fact] + public void PartialCbufferMetadataKeepsRawRegisterOffsets() + { + HlslShaderNode shader = new HlslShaderNode { Name = "Test/CbufferLayout" }; + HlslSubShaderNode subShader = new HlslSubShaderNode(); + HlslPassNode pass = new HlslPassNode(); + HlslResourceNode resource = new HlslResourceNode + { + Name = "NamedBuffer", Kind = HlslResourceKind.ConstantBuffer, Slot = 1, + }; + resource.Variables.Add(new HlslCBufferVariable + { + Name = "NamedValue", TypeName = "float4", Offset = 0, Size = 16, + }); + resource.Variables.Add(new HlslCBufferVariable + { + Name = "cb1_values", TypeName = "float4", Offset = 0, Size = 32, ArraySize = 2, + }); + pass.Resources.Add(resource); + IRRegister read = new IRRegister + { + RegisterType = RegisterType.ConstantBuffer, + Type = IRValueType.Float, + Mask = 15, + ComponentMode = Operand.OperandComponentMode.Mask, + }; + read.Indices.Add(1); + read.Indices.Add(0); + HlslFunctionNode function = new HlslFunctionNode { Name = "frag", Stage = HlslShaderStage.Fragment }; + function.Statements.Statements.Add(new HlslAssignmentStatement + { + Destination = Temp(0, 15), + Expression = new IRExpression.RegisterExpression { Register = read }, + }); + pass.FragmentFunction = function; + subShader.Passes.Add(pass); + shader.SubShaders.Add(subShader); + + string source = HlslPrettyPrinter.Print(shader, fuseTemps: false); + Assert.Contains("cbuffer NamedBuffer : register(b1) { float4 cb1_values[2]; };", source); + Assert.Contains("cb1_values[0]", source); + Assert.DoesNotContain("NamedValue", source); + } + + private static string Print(HlslFunctionNode function) + { + HlslShaderNode shader = new HlslShaderNode { Name = "Test/Scope" }; + HlslSubShaderNode subShader = new HlslSubShaderNode(); + subShader.Passes.Add(new HlslPassNode { FragmentFunction = function }); + shader.SubShaders.Add(subShader); + return HlslPrettyPrinter.Print(shader, fuseTemps: false); + } + + private static IRRegister Temp(uint index, byte mask = 1) + { + IRRegister register = new IRRegister + { + RegisterType = RegisterType.Temp, + Index = index, + Type = IRValueType.Float, + Mask = mask, + ComponentMode = Operand.OperandComponentMode.Mask, + }; + for (int component = 0; component < 4; component++) + if ((mask & (1 << component)) != 0) + register.SsaVersion[component] = 1; + return register; + } + + private static IRExpression.ConstantExpression Float(float value) => new() + { + RawValues = new[] { unchecked((uint)BitConverter.SingleToInt32Bits(value)) }, + }; +} diff --git a/tests/UnityShaderDecompiler.Tests/SamplerBindingTests.cs b/tests/UnityShaderDecompiler.Tests/SamplerBindingTests.cs new file mode 100644 index 0000000..9831678 --- /dev/null +++ b/tests/UnityShaderDecompiler.Tests/SamplerBindingTests.cs @@ -0,0 +1,22 @@ +using System.Linq; +using Parser; +using Parser.Core.Hlsl.Ast; + +namespace UnityShaderDecompiler.Tests; + +public class SamplerBindingTests +{ + [Fact] + public void SameRecoveredNameDoesNotMergeDifferentSamplerSlots() + { + DecompilationResult result = FixtureCache.Decompile("HOLO_Holo"); + HlslPassNode pass = Assert.Single(result.Passes.Where(p => + p.Resources.Any(r => r.Kind == HlslResourceKind.Sampler && r.Slot == 2))); + HlslResourceNode first = Assert.Single(pass.Resources, + r => r.Kind == HlslResourceKind.Sampler && r.Slot == 0); + HlslResourceNode second = Assert.Single(pass.Resources, + r => r.Kind == HlslResourceKind.Sampler && r.Slot == 2); + Assert.NotEqual(first.Name, second.Name); + Assert.Contains("SamplerState " + second.Name, result.ShaderText); + } +} From 799bba4822ebb06bc017a1080aa9830de32f893b Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 12:20:13 +0100 Subject: [PATCH 09/93] Preserve register bits and arithmetic in no-fuse mode --- Shader Decompiler/Core/Hlsl/HlslAST.cs | 1 + .../Core/Hlsl/HlslPrettyPrinter.cs | 72 +++++-- .../DXBC/IR/Builders/IRBuilderArithmetic.cs | 7 +- Shader Decompiler/DXBC/IR/IRBuilder.cs | 42 ++-- Shader Decompiler/DXBC/IR/IRExpression.cs | 3 + Shader Decompiler/DXBC/IR/IRPipeline.cs | 7 +- Shader Decompiler/Decompiler.cs | 3 +- docs/pr-raw-register-semantics.md | 7 + .../RawRegisterSemanticsTests.cs | 188 ++++++++++++++++++ 9 files changed, 296 insertions(+), 34 deletions(-) create mode 100644 docs/pr-raw-register-semantics.md create mode 100644 tests/UnityShaderDecompiler.Tests/RawRegisterSemanticsTests.cs diff --git a/Shader Decompiler/Core/Hlsl/HlslAST.cs b/Shader Decompiler/Core/Hlsl/HlslAST.cs index 93478c0..144323c 100644 --- a/Shader Decompiler/Core/Hlsl/HlslAST.cs +++ b/Shader Decompiler/Core/Hlsl/HlslAST.cs @@ -231,6 +231,7 @@ public enum HlslShaderStage public sealed class HlslFunctionNode { + public bool RawTemporaryStorage { get; set; } // Canonical per-stage name (Stage 9): vert/frag/geom/hull/domain/comp. // No collision risk between passes — each Pass has its own function // slots, so every pass's vertex function is just called "vert". diff --git a/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs b/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs index 222df18..46cda5b 100644 --- a/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs +++ b/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs @@ -564,6 +564,9 @@ private sealed class PrintContext // used to render RDEF-less cbN[slot] reads as real variable names. public Dictionary Cbuffers { get; set; } = new(); + public bool PreserveArithmetic { get; init; } + public bool RawTemporaryStorage { get; init; } + // Identifier each defining instruction actually declared for a // value, keyed by the value's structural identity (storage location // + SSA version), NOT by its rendered name string. Reads resolve @@ -648,6 +651,8 @@ private static void PrintFunction( { Cbuffers = cbuffers, Stage = fn.Stage, + PreserveArithmetic = !fuseTemps, + RawTemporaryStorage = fn.RawTemporaryStorage, InputElements = fn.InputElementsByRegister, OutputElements = fn.OutputElementsByRegister, }; @@ -690,7 +695,7 @@ private static void PrintFunction( // multiplies (CollapseMatrixMul), then common math idioms (normalize, // dot, lerp, vector-by-matrix mul) simplify. Must run after // PrintBlock so every use in the body is visible. - sb.Append(HlslMathSimplify.Simplify(body.ToString())); + sb.Append(fuseTemps ? HlslMathSimplify.Simplify(body.ToString()) : body.ToString()); sb.Append(" }\n"); } @@ -732,7 +737,9 @@ private static void PrintBlock(StringBuilder sb, HlslBlockStatement block, int i for (int k = i; k < j; k++) lines.AddRange(RenderStatementLines(stmts[k], ctx)); - foreach (string line in CollapseExtractChains(RevertUnityMacros(lines, ctx), ctx)) + foreach (string line in ctx.PreserveArithmetic + ? lines + : CollapseExtractChains(RevertUnityMacros(lines, ctx), ctx)) sb.Append(pad).Append(line).Append('\n'); i = j; } @@ -1485,7 +1492,7 @@ private static void PrintStatement(StringBuilder sb, HlslStatementNode stmt, int { if (!ctx.Declared.Add(name)) continue; // already in scope — don't redeclare - sb.Append(pad).Append(DeclType(comps)).Append(' ').Append(name).Append(";\n"); + sb.Append(pad).Append(DeclType(comps, ctx)).Append(' ').Append(name).Append(";\n"); } sb.Append(pad).Append("if (").Append(RenderExpression(iff.Condition, ctx)).Append(")\n"); @@ -1516,7 +1523,7 @@ private static void PrintStatement(StringBuilder sb, HlslStatementNode stmt, int { if (!ctx.Declared.Add(name)) continue; // already in scope — don't redeclare - sb.Append(pad).Append(DeclType(comps)).Append(' ').Append(name).Append(";\n"); + sb.Append(pad).Append(DeclType(comps, ctx)).Append(' ').Append(name).Append(";\n"); } sb.Append(pad).Append("[loop]\n").Append(pad).Append("while (true)\n").Append(pad).Append("{\n"); @@ -1847,7 +1854,7 @@ private static string RenderTempRead(IRRegister reg, PrintContext ctx) : $"{firstName}.{readLetters}"; } - return $"float{active.Count}({string.Join(", ", resolved.Select(r => ComponentView(r.Name, r.Active, r.Comp)))})"; + return $"{DeclType(active.Count, ctx)}({string.Join(", ", resolved.Select(r => ComponentView(r.Name, r.Active, r.Comp)))})"; } // Renders one component of a declared name: a scalar declaration @@ -1934,6 +1941,8 @@ private static string RenderRegisterRead(IRRegister reg, PrintContext ctx) BaseIdentifier(reg, ctx) + MaskOrSwizzleSuffix(reg), }; + if (ctx.RawTemporaryStorage && reg.RegisterType is RegisterType.Temp or RegisterType.IndexableTemp) + rendered = $"asfloat({rendered})"; return ApplyRegisterModifier(reg.Modifier, rendered); } @@ -2055,26 +2064,34 @@ private static IEnumerable RenderOutputWrite(IRRegister dest, string rhs if (groups.Count == 1 && groups[0].Positions.Count == active.Count) { var (el, pos) = groups[0]; - yield return $"o.{ElementFieldName(el)}{ElementLocalSuffix(el, pos)} = {rhs};"; + yield return $"o.{ElementFieldName(el)}{ElementLocalSuffix(el, pos)} = {OutputValue(rhs, el, ctx)};"; } else if (groups.Count > 1) { foreach ((SignatureElement el, List pos) in groups) { string slice = string.Concat(pos.Select(p => ComponentLetters[p])); - yield return $"o.{ElementFieldName(el)}{ElementLocalSuffix(el, pos)} = ({rawRhs}).{slice};"; + yield return $"o.{ElementFieldName(el)}{ElementLocalSuffix(el, pos)} = {OutputValue($"({rawRhs}).{slice}", el, ctx)};"; } } else { - yield return $"o.{FieldNameFor(dest, ctx)}{MaskOrSwizzleSuffix(dest)} = {rhs};"; + yield return $"o.{FieldNameFor(dest, ctx)}{MaskOrSwizzleSuffix(dest)} = {OutputValue(rhs, null, ctx)};"; } yield break; } - yield return $"o.{FieldNameFor(dest, ctx)}{MaskOrSwizzleSuffix(dest)} = {rhs};"; + yield return $"o.{FieldNameFor(dest, ctx)}{MaskOrSwizzleSuffix(dest)} = {OutputValue(rhs, elements?.FirstOrDefault(), ctx)};"; } + private static string OutputValue(string bits, SignatureElement? element, PrintContext ctx) => + !ctx.RawTemporaryStorage ? bits : element?.ComponentType switch + { + 1 => bits, + 2 => $"asint({bits})", + _ => $"asfloat({bits})", + }; + // Emits one or more statement lines for a single destination write. // Multiple lines only happen when the written components have // diverged to different SSA versions (see below) — the common case @@ -2133,12 +2150,14 @@ private static IEnumerable RenderAssignmentLines(IRRegister dest, IRExpr continue; string scalar = ScalarIdentifier(src, c, ctx); if (ctx.Declared.Add(scalar)) - yield return $"float {scalar};"; + yield return $"{DeclType(1, ctx)} {scalar};"; } } } string rawRhs = RenderExpression(expr, ctx); + if (ctx.RawTemporaryStorage && dest.RegisterType is RegisterType.Temp or RegisterType.IndexableTemp or RegisterType.Output) + rawRhs = EncodeRegisterBits(rawRhs, expr); // DXBC computes full-lane expressions (the source operand's 4-wide // swizzle) regardless of how many components the destination @@ -2177,7 +2196,7 @@ private static IEnumerable RenderAssignmentLines(IRRegister dest, IRExpr ctx.DeclaredViews[(LocationOf(dest, c), vv)] = (name, active); yield return ctx.Declared.Add(name) - ? $"{DeclType(active.Count)} {name} = {rhs};" + ? $"{DeclType(active.Count, ctx)} {name} = {rhs};" : $"{name} = {rhs};"; yield break; } @@ -2194,8 +2213,11 @@ private static IEnumerable RenderAssignmentLines(IRRegister dest, IRExpr string scalarName = ScalarIdentifier(dest, c, ctx); if (dest.SsaVersion[c] is { } v) ctx.DeclaredViews[(LocationOf(dest, c), v)] = (scalarName, new List { c }); - string line = $"{scalarName} = {ExtractComponent(expr, c, ctx)};"; - yield return ctx.Declared.Add(scalarName) ? $"float {line}" : line; + string component = ExtractComponent(expr, c, ctx); + if (ctx.RawTemporaryStorage) + component = EncodeRegisterBits(component, expr); + string line = $"{scalarName} = {component};"; + yield return ctx.Declared.Add(scalarName) ? $"{DeclType(1, ctx)} {line}" : line; } } @@ -2228,12 +2250,25 @@ private static string ExtractComponent(IRExpression expr, int component, PrintCo Operand.OperandComponentMode.Select1 => r.Component, _ => pos, }; - return ResolveComponent(r, source, ctx); + string value = ResolveComponent(r, source, ctx); + return ctx.RawTemporaryStorage ? $"asfloat({value})" : value; } return $"({RenderExpression(expr, ctx)}).{ComponentLetters[component]}"; } - private static string DeclType(int componentCount) => componentCount == 1 ? "float" : $"float{componentCount}"; + private static string DeclType(int componentCount, PrintContext ctx) + { + string type = ctx.RawTemporaryStorage ? "uint" : "float"; + return componentCount == 1 ? type : $"{type}{componentCount}"; + } + + private static string EncodeRegisterBits(string value, IRExpression expression) => expression switch + { + IRExpression.RegisterExpression => $"asuint({value})", + _ when expression.Type == IRValueType.Bool => $"(({value}) ? 0xffffffffu : 0u)", + _ when expression.Type == IRValueType.UInt => value, + _ => $"asuint({value})", + }; // HLSL (like C) won't assign a wide vector into a narrower L-value, but // DXBC's per-lane semantics mean the RHS expression naturally renders at @@ -2550,6 +2585,8 @@ or IRExpression.BinaryOperation.LessThan private static string RenderBitwise(IRExpression.BinaryExpression b, PrintContext ctx) { + if (ctx.RawTemporaryStorage) + return $"({RenderExpression(b.Left, ctx)} {BinaryOpText(b.Operation)} {RenderExpression(b.Right, ctx)})"; string Op(IRExpression side) { string s = RenderExpression(side, ctx); @@ -2591,6 +2628,11 @@ private static string RenderIntrinsic(IRExpression.IntrinsicExpression expr, Pri string Arg(int n) => RenderExpression(expr.Arguments[n], ctx); string AllArgs() => string.Join(", ", expr.Arguments.Select(a => RenderExpression(a, ctx))); + // fxc folds frac(x + integer) even with /Gis, losing the add's rounding. + // Use the DXBC definition of frc to retain that dependency. + if (ctx.RawTemporaryStorage && expr.Intrinsic == IRExpression.IRIntrinsic.FractionalPart) + return $"({Arg(0)} - floor({Arg(0)}))"; + // Casts use C-style cast syntax, not a function call. switch (expr.Intrinsic) { diff --git a/Shader Decompiler/DXBC/IR/Builders/IRBuilderArithmetic.cs b/Shader Decompiler/DXBC/IR/Builders/IRBuilderArithmetic.cs index 5440f27..3810f93 100644 --- a/Shader Decompiler/DXBC/IR/Builders/IRBuilderArithmetic.cs +++ b/Shader Decompiler/DXBC/IR/Builders/IRBuilderArithmetic.cs @@ -8,7 +8,8 @@ public partial class IRBuilder private void BuildMov(IRProgram program, Instruction instruction) { - Emit(program, instruction, BuildExpression(instruction.Operands[1])); + Emit(program, instruction, _preserveRegisterBits + ? BuildRawExpression(instruction.Operands[1]) : BuildExpression(instruction.Operands[1])); } private void BuildMovC(IRProgram program, Instruction instruction) @@ -17,8 +18,8 @@ private void BuildMovC(IRProgram program, Instruction instruction) new IRExpression.ConditionalExpression { Condition = BuildBoolExpression(instruction.Operands[1]), - TrueExpression = BuildExpression(instruction.Operands[2]), - FalseExpression = BuildExpression(instruction.Operands[3]) + TrueExpression = _preserveRegisterBits ? BuildRawExpression(instruction.Operands[2]) : BuildExpression(instruction.Operands[2]), + FalseExpression = _preserveRegisterBits ? BuildRawExpression(instruction.Operands[3]) : BuildExpression(instruction.Operands[3]) }; Emit(program, instruction, expression); diff --git a/Shader Decompiler/DXBC/IR/IRBuilder.cs b/Shader Decompiler/DXBC/IR/IRBuilder.cs index 6c9c618..9ceab4d 100644 --- a/Shader Decompiler/DXBC/IR/IRBuilder.cs +++ b/Shader Decompiler/DXBC/IR/IRBuilder.cs @@ -4,6 +4,9 @@ namespace Parser.DXBC.IR; public partial class IRBuilder { + private readonly bool _preserveRegisterBits; + + public IRBuilder(bool preserveRegisterBits = false) => _preserveRegisterBits = preserveRegisterBits; private readonly Dictionary<(RegisterType Type, uint Index), IRValueType> _registerTypes = new(); public IRProgram Build(ShdrParser parser) @@ -483,10 +486,7 @@ private IRExpression BuildExpression(Operand operand) }; } - return new IRExpression.RegisterExpression - { - Register = BuildRegister(operand) - }; + return TypedRead(operand, IRExpression.IRIntrinsic.AsFloat); } private IRExpression BuildIntExpression(Operand operand) @@ -500,10 +500,7 @@ private IRExpression BuildIntExpression(Operand operand) }; } - return new IRExpression.RegisterExpression - { - Register = BuildRegister(operand) - }; + return TypedRead(operand, IRExpression.IRIntrinsic.AsInt); } private IRExpression BuildUIntExpression(Operand operand) @@ -517,11 +514,32 @@ private IRExpression BuildUIntExpression(Operand operand) }; } - return new IRExpression.RegisterExpression + return TypedRead(operand, IRExpression.IRIntrinsic.AsUInt); + } + + private IRExpression TypedRead(Operand operand, IRExpression.IRIntrinsic interpretation) + { + IRRegister register = BuildRegister(operand); + IRExpression value = new IRExpression.RegisterExpression { Register = register }; + if (!_preserveRegisterBits) return value; + ShdrParser.OperandModifier modifier = register.Modifier; + register.Modifier = ShdrParser.OperandModifier.None; + value = new IRExpression.IntrinsicExpression { Intrinsic = interpretation, Arguments = { value } }; + // Integer negation is two's complement, not a float sign-bit flip. + if (modifier is ShdrParser.OperandModifier.Abs or ShdrParser.OperandModifier.AbsNeg) + value = new IRExpression.UnaryExpression { Operation = IRExpression.UnaryExpression.UnaryOperation.Absolute, Operand = value }; + if (modifier is ShdrParser.OperandModifier.Neg or ShdrParser.OperandModifier.AbsNeg) + value = new IRExpression.UnaryExpression { Operation = IRExpression.UnaryExpression.UnaryOperation.Negate, Operand = value }; + return value; + } + + private IRExpression BuildRawExpression(Operand operand) => operand.RegisterType == RegisterType.Immediate32 + ? BuildUIntExpression(operand) + : new IRExpression.IntrinsicExpression { - Register = BuildRegister(operand) + Intrinsic = IRExpression.IRIntrinsic.AsUInt, + Arguments = { BuildExpression(operand) }, }; - } private IRExpression BuildDoubleExpression(Operand operand) { @@ -542,7 +560,7 @@ private IRExpression BuildDoubleExpression(Operand operand) private IRExpression BuildBoolExpression(Operand operand) { - IRExpression expression = BuildExpression(operand); + IRExpression expression = _preserveRegisterBits ? BuildUIntExpression(operand) : BuildExpression(operand); if (expression.Type == IRValueType.Bool) return expression; diff --git a/Shader Decompiler/DXBC/IR/IRExpression.cs b/Shader Decompiler/DXBC/IR/IRExpression.cs index 1be6411..34b46b6 100644 --- a/Shader Decompiler/DXBC/IR/IRExpression.cs +++ b/Shader Decompiler/DXBC/IR/IRExpression.cs @@ -317,6 +317,9 @@ private IRValueType GetFirstArgumentType() IRIntrinsic.CastUInt => IRValueType.UInt, IRIntrinsic.CastDouble => IRValueType.Double, IRIntrinsic.CastBool => IRValueType.Bool, + IRIntrinsic.AsFloat => IRValueType.Float, + IRIntrinsic.AsInt => IRValueType.Int, + IRIntrinsic.AsUInt => IRValueType.UInt, IRIntrinsic.Any => IRValueType.Bool, IRIntrinsic.All => IRValueType.Bool, diff --git a/Shader Decompiler/DXBC/IR/IRPipeline.cs b/Shader Decompiler/DXBC/IR/IRPipeline.cs index 60e1ef1..e9bc053 100644 --- a/Shader Decompiler/DXBC/IR/IRPipeline.cs +++ b/Shader Decompiler/DXBC/IR/IRPipeline.cs @@ -24,7 +24,7 @@ public sealed class Result public List<(string Phase, string Text)> StageDumps { get; } = new(); } - public static Result Run(DxbcFile file, bool dumpStages = false) + public static Result Run(DxbcFile file, bool dumpStages = false, bool preserveArithmetic = false) { var stageDumps = new List<(string Phase, string Text)>(); @@ -42,7 +42,7 @@ void Snapshot(string phase, List blocks) } // 1. Decode -> flat IR (no control flow structure yet) - var builder = new IRBuilder(); + IRBuilder builder = new IRBuilder(preserveRegisterBits: preserveArithmetic); IRProgram program = builder.Build(file.Shader!); // 2. Split into basic blocks and wire up predecessors/successors @@ -77,7 +77,8 @@ void Snapshot(string phase, List blocks) // is read-only so it doesn't matter when it runs relative to // the others, but doing it here means it sees the same // simplified shape as everything else. - IRShaderPatternRecognition.Run(blocks); + if (!preserveArithmetic) + IRShaderPatternRecognition.Run(blocks); Snapshot("05-pattern-recognized", blocks); List loops = IRShaderPatternRecognition.DetectLoops(blocks); diff --git a/Shader Decompiler/Decompiler.cs b/Shader Decompiler/Decompiler.cs index 5a44dad..7dce58d 100644 --- a/Shader Decompiler/Decompiler.cs +++ b/Shader Decompiler/Decompiler.cs @@ -221,7 +221,7 @@ public static DecompilationResult Decompile( log.WriteLine($"Saved: {dxbcPath}"); } - IRPipeline.Result pipelineResult = IRPipeline.Run(dxbcFile, options.DumpStages); + IRPipeline.Result pipelineResult = IRPipeline.Run(dxbcFile, options.DumpStages, preserveArithmetic: !options.FuseTemps); if (options.DumpStages) { @@ -314,6 +314,7 @@ public static DecompilationResult Decompile( } else { + function.RawTemporaryStorage = !options.FuseTemps; candidates.Add(new DecompiledSubprogram { Index = i, diff --git a/docs/pr-raw-register-semantics.md b/docs/pr-raw-register-semantics.md new file mode 100644 index 0000000..ac3160d --- /dev/null +++ b/docs/pr-raw-register-semantics.md @@ -0,0 +1,7 @@ +# Preserve register bits and arithmetic in no-fuse mode + +Keep temporary registers as raw uint bits with explicit typed reads, full-bit comparison masks and integer source modifiers applied after interpretation. Disable arithmetic pattern rewrites in no-fuse mode and preserve the addition before fractional-part evaluation under native recompilation. + +Validation: 54 C# tests and all four existing PicaVoxel extraction/decompilation tests pass. C# tests use an en-US default culture; the upstream fr-FR golden-output failure is addressed independently by the numeric-formatting PR. + +This branch is based directly on upstream `1850608`; it does not include the other review branches. No proprietary fixtures or machine-specific paths are added. diff --git a/tests/UnityShaderDecompiler.Tests/RawRegisterSemanticsTests.cs b/tests/UnityShaderDecompiler.Tests/RawRegisterSemanticsTests.cs new file mode 100644 index 0000000..1788b4d --- /dev/null +++ b/tests/UnityShaderDecompiler.Tests/RawRegisterSemanticsTests.cs @@ -0,0 +1,188 @@ +using System; +using System.Linq; +using Parser.Core.Hlsl; +using Parser.Core.Hlsl.Ast; +using Parser.Core.Analysis; +using Parser.Core.Optimizations; +using Parser.DXBC.Instructions; +using Parser.DXBC.IR; +using Xunit; + +namespace UnityShaderDecompiler.Tests; + +public sealed class RawRegisterSemanticsTests +{ + [Fact] + public void IntegerSourceModifierIsAppliedAfterBitInterpretation() + { + ShdrParser parser = new ShdrParser(); + parser.Instructions.Add(new Instruction + { + Opcode = Opcode.IMax, + Operands = + { + new Operand { RegisterType = RegisterType.Temp, RegisterIndex = 1, Mask = 1 }, + new Operand { RegisterType = RegisterType.Temp, RegisterIndex = 0, Mask = 1, + Modifier = ShdrParser.OperandModifier.Neg }, + new Operand { RegisterType = RegisterType.Immediate32, Immediate32Values = new uint[] { 0 } }, + }, + }); + IRProgram program = new IRBuilder(preserveRegisterBits: true).Build(parser); + IRStatement.IRAssignment assignment = Assert.IsType(Assert.Single(program.Statements)); + IRExpression.IntrinsicExpression maximum = Assert.IsType(assignment.Expression); + IRExpression.UnaryExpression negation = Assert.IsType(maximum.Arguments[0]); + Assert.Equal(IRExpression.UnaryExpression.UnaryOperation.Negate, negation.Operation); + Assert.Equal(IRValueType.Int, negation.Type); + IRExpression.IntrinsicExpression cast = Assert.IsType(negation.Operand); + Assert.Equal(IRExpression.IRIntrinsic.AsInt, cast.Intrinsic); + IRExpression.RegisterExpression register = Assert.IsType(Assert.Single(cast.Arguments)); + Assert.Equal(ShdrParser.OperandModifier.None, register.Register.Modifier); + } + + [Fact] + [Trait("Category", "D3D")] + public void FractionalPartRetainsPrecedingAdditionWhenRecompiled() + { + if (!OperatingSystem.IsWindows()) return; + + Parser.DXBC.Chunks.SignatureElement input = new Parser.DXBC.Chunks.SignatureElement + { + SemanticName = "TEXCOORD", Mask = 1, ComponentType = 3, + }; + Parser.DXBC.Chunks.SignatureElement output = new Parser.DXBC.Chunks.SignatureElement + { + SemanticName = "SV_Target", Mask = 1, ComponentType = 3, + }; + HlslFunctionNode function = new HlslFunctionNode + { + Name = "frag", Stage = HlslShaderStage.Fragment, RawTemporaryStorage = true, + InputStruct = new HlslStructNode { Name = "TestInput" }, + OutputStruct = new HlslStructNode { Name = "TestOutput" }, + }; + function.InputStruct.Fields.Add(new HlslFieldNode + { + Name = HlslAstBuilder.ToFieldName("TEXCOORD0"), Semantic = "TEXCOORD0", TypeHint = "float", + }); + function.OutputStruct.Fields.Add(new HlslFieldNode + { + Name = HlslAstBuilder.ToFieldName("SV_Target0"), Semantic = "SV_Target0", TypeHint = "float", + }); + function.InputElementsByRegister[0] = new() { input }; + function.OutputElementsByRegister[0] = new() { output }; + IRRegister source = Temp(0); + source.RegisterType = RegisterType.Input; + source.SymbolicName = "TEXCOORD0"; + IRRegister destination = Temp(0); + destination.RegisterType = RegisterType.Output; + destination.SymbolicName = "SV_Target0"; + function.Statements.Statements.Add(new HlslAssignmentStatement + { + Destination = Temp(1), + Expression = new IRExpression.BinaryExpression + { + Operation = IRExpression.BinaryOperation.Add, + Left = new IRExpression.RegisterExpression { Register = source }, Right = Float(1), + }, + }); + function.Statements.Statements.Add(new HlslAssignmentStatement + { + Destination = destination, + Expression = new IRExpression.IntrinsicExpression + { + Intrinsic = IRExpression.IRIntrinsic.FractionalPart, + Arguments = { new IRExpression.RegisterExpression { Register = Temp(1) } }, + }, + }); + function.Statements.Statements.Add(new HlslReturnStatement()); + HlslShaderNode shader = new HlslShaderNode { Name = "Test/Fraction" }; + HlslSubShaderNode subShader = new HlslSubShaderNode(); + HlslPassNode pass = new HlslPassNode { FragmentFunction = function }; + pass.Structs.Add(function.InputStruct); + pass.Structs.Add(function.OutputStruct); + subShader.Passes.Add(pass); + shader.SubShaders.Add(subShader); + string text = HlslPrettyPrinter.Print(shader, fuseTemps: false); + string hlsl = System.Text.RegularExpressions.Regex.Match(text, + @"HLSLPROGRAM(.*?)ENDHLSL", System.Text.RegularExpressions.RegexOptions.Singleline).Groups[1].Value; + Parser.DXBC.DxbcFile dxbc = new Parser.DXBC.DxbcFile(); + dxbc.Load(Parser.RecompileVerify.CompileHlsl(hlsl, "frag", "ps_4_0")); + + // frac(x + 1) loses the add under fxc optimization; round_ni must keep it. + System.Collections.Generic.List instructions = dxbc.Shader!.Instructions; + int floor = instructions.FindIndex(i => i.Opcode == Opcode.RoundNI); + Assert.True(floor > 0); + Assert.Contains(instructions.Take(floor), i => i.Opcode == Opcode.Add); + Assert.Contains(instructions.Skip(floor + 1), i => i.Opcode == Opcode.Add); + } + + [Fact] + public void RawTemporaryStorageKeepsComparisonMasksAndIntegerArithmetic() + { + HlslFunctionNode function = new HlslFunctionNode + { + Name = "frag", Stage = HlslShaderStage.Fragment, RawTemporaryStorage = true, + }; + function.Statements.Statements.Add(new HlslAssignmentStatement + { + Destination = Temp(0), + Expression = new IRExpression.BinaryExpression + { + Operation = IRExpression.BinaryOperation.LessThan, + Left = Float(1), Right = Float(2), + }, + }); + IRExpression.IntrinsicExpression signedRead = new IRExpression.IntrinsicExpression + { + Intrinsic = IRExpression.IRIntrinsic.AsInt, + Arguments = { new IRExpression.RegisterExpression { Register = Temp(0) } }, + }; + function.Statements.Statements.Add(new HlslAssignmentStatement + { + Destination = Temp(1), + Expression = new IRExpression.BinaryExpression + { + Operation = IRExpression.BinaryOperation.Add, + Left = signedRead, + Right = new IRExpression.ConstantExpression + { + Kind = IRExpression.ConstantExpression.ConstantKind.Int, RawValues = new uint[] { 2 }, + }, + }, + }); + string source = Print(function); + Assert.Contains("uint r0_x_1", source); + Assert.Contains("? 0xffffffffu : 0u", source); + Assert.Contains("asint(asfloat(r0_x_1))", source); + Assert.Contains("uint r1_x_1 = asuint(", source); + } + + private static string Print(HlslFunctionNode function) + { + HlslShaderNode shader = new HlslShaderNode { Name = "Test/Scope" }; + HlslSubShaderNode subShader = new HlslSubShaderNode(); + subShader.Passes.Add(new HlslPassNode { FragmentFunction = function }); + shader.SubShaders.Add(subShader); + return HlslPrettyPrinter.Print(shader, fuseTemps: false); + } + + private static IRRegister Temp(uint index, byte mask = 1) + { + IRRegister register = new IRRegister + { + RegisterType = RegisterType.Temp, + Index = index, + Type = IRValueType.Float, + Mask = mask, + ComponentMode = Operand.OperandComponentMode.Mask, + }; + for (int component = 0; component < 4; component++) + if ((mask & (1 << component)) != 0) + register.SsaVersion[component] = 1; + return register; + } + + private static IRExpression.ConstantExpression Float(float value) => new() + { + RawValues = new[] { unchecked((uint)BitConverter.SingleToInt32Bits(value)) }, + }; +} From cb268f774b6987d1c6b641328de2055553641c7d Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 12:21:24 +0100 Subject: [PATCH 10/93] Normalize AssetStudio and AssetRipper shader JSON at extraction --- Extract.py | 4 +- docs/pr-metadata-offsets.md | 10 ++- libs/blob.py | 7 +- libs/metadata.py | 38 ++++----- libs/shader_json.py | 116 ++++++++++++++++++++++++++++ tests/test_exporter_schemas.py | 136 +++++++++++++++++++++++++++++++++ 6 files changed, 278 insertions(+), 33 deletions(-) create mode 100644 libs/shader_json.py create mode 100644 tests/test_exporter_schemas.py diff --git a/Extract.py b/Extract.py index 60107e8..b2aa520 100644 --- a/Extract.py +++ b/Extract.py @@ -30,7 +30,7 @@ def safe_folder_name(name: str) -> str: def main(): ap = argparse.ArgumentParser() - ap.add_argument("json_path", help="Path to the AssetStudio shader JSON export") + ap.add_argument("json_path", help="Path to an AssetStudio or AssetRipper shader JSON export") ap.add_argument("--dummy-dir", default="shaders_dummy", help="Directory of AssetStudio plain-text dummy exports") ap.add_argument("--out-dir", default="Output", help="Root output directory") @@ -77,4 +77,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/docs/pr-metadata-offsets.md b/docs/pr-metadata-offsets.md index 1692765..d1c1854 100644 --- a/docs/pr-metadata-offsets.md +++ b/docs/pr-metadata-offsets.md @@ -1,5 +1,9 @@ -# Accept both serialized constant-buffer offset names +# Accept AssetStudio and AssetRipper shader JSON schemas -Read `m_OffsetInConstantBuffer` when present and the legacy `m_Index` otherwise. AssetRipper's `Pass002_RenameSubnodes` explicitly normalizes that rename for vector, matrix and struct parameters. Both layout readers now reject absent, negative, unaligned or non-integer offsets instead of inventing zero. +Normalize the two exporters' field names, serialized maps, property defaults and render states at the JSON input boundary. Recover the ShaderLab name from ParsedForm when the object name is empty. Both metadata extraction and blob extraction use the same loader; the original JSON is never rewritten. -Validation: five Python regression tests and all four existing PicaVoxel extraction/decompilation tests pass. All 51 baseline C# tests pass with an en-US default culture. Under the machine's fr-FR default, the unchanged golden test fails on decimal separators; that pre-existing defect is addressed separately by the numeric-formatting PR. No existing assertions or golden files are changed here. +Read `m_OffsetInConstantBuffer` when present and legacy `m_Index` otherwise. AssetRipper's `Pass002_RenameSubnodes` explicitly normalizes this rename. Both layout readers reject invalid or missing offsets instead of inventing zero. Conflicting aliases and duplicate map keys also fail explicitly. + +Validation: ten generic Python tests and all four existing PicaVoxel extraction/decompilation tests pass. Two additional local shaders were exported directly from the same game bundle with AssetStudioMod CLI v0.20.0 and AssetRipper: decompressed blobs and extracted metadata match across exporters. Those external fixtures are not included in this PR. + +All 51 baseline C# tests pass with an en-US default culture. The unchanged golden test fails under fr-FR on upstream because of decimal separators; the numeric-formatting PR fixes that independently. No existing assertions or golden files are changed here. diff --git a/libs/blob.py b/libs/blob.py index 0f97528..7377379 100644 --- a/libs/blob.py +++ b/libs/blob.py @@ -1,7 +1,7 @@ import base64 -import json from .lz4util import decompress +from .shader_json import load_shader_json class ShaderBlob: @@ -9,8 +9,7 @@ class ShaderBlob: def __init__(self, json_path): self.json_path = json_path - with open(json_path, "r", encoding="utf8") as f: - self.data = json.load(f) + self.data = load_shader_json(json_path) @property def name(self): @@ -28,4 +27,4 @@ def get_blob(self): compressed, compressed_length, decompressed_length, - ) \ No newline at end of file + ) diff --git a/libs/metadata.py b/libs/metadata.py index 82556c7..6e3ec20 100644 --- a/libs/metadata.py +++ b/libs/metadata.py @@ -1,20 +1,20 @@ """ -Stage 0.2 — Metadata Extraction +Stage 0.2 — Metadata Extraction Pulls everything the backend (C#) needs out of the raw Unity shader JSON (the AssetStudio export) into a single metadata.json, keyed by shader name. -This does NOT touch m_CompressedBlob — that stays in blob.py/ShaderBlob. +This does NOT touch m_CompressedBlob — that stays in blob.py/ShaderBlob. """ import json +from .shader_json import load_shader_json, parameter_offset class ShaderMetadata: def __init__(self, json_path): self.json_path = json_path - with open(json_path, "r", encoding="utf8") as f: - self.data = json.load(f) + self.data = load_shader_json(json_path) def build(self): d = self.data @@ -39,16 +39,6 @@ def save(self, out_path): # -- helpers -- - @staticmethod - def _parameter_offset(parameter): - # Unity renamed this field; AssetRipper also normalizes older exports. - field = "m_OffsetInConstantBuffer" if "m_OffsetInConstantBuffer" in parameter else "m_Index" - offset = parameter.get(field) - if type(offset) is not int or offset < 0 or offset % 4: - raise ValueError( - f"Invalid or missing constant-buffer byte offset for parameter {parameter.get('m_NameIndex')}: {offset!r}") - return offset - @staticmethod def _get(d, key, default=None): return d.get(key, default) if isinstance(d, dict) else default @@ -117,7 +107,7 @@ def _name_layouts(raw_passes): for v in b.get("m_VectorParams", []) or []: entry = { "name": index_to_name.get(v.get("m_NameIndex"), ""), - "offset": ShaderMetadata._parameter_offset(v), + "offset": parameter_offset(v), "dim": v.get("m_Dim", 0), "arraySize": v.get("m_ArraySize", 0), } @@ -127,7 +117,7 @@ def _name_layouts(raw_passes): for m in b.get("m_MatrixParams", []) or []: entry = { "name": index_to_name.get(m.get("m_NameIndex"), ""), - "offset": ShaderMetadata._parameter_offset(m), + "offset": parameter_offset(m), "rowCount": m.get("m_RowCount", 0), "arraySize": m.get("m_ArraySize", 0), "isMatrix": True, @@ -172,11 +162,11 @@ def _pass(self, p, name_layouts=None): def _constant_buffers(self, p, prog_keys, index_to_name, name_layouts=None): # Unity serializes the constant-buffer layout (name + per-variable - # byte offset) in each stage's m_CommonParameters — this is the + # byte offset) in each stage's m_CommonParameters — this is the # per-pass replacement for the RDEF chunk Unity strips from shipped # bytecode. Each stage binds its OWN table (cb0 in the vertex # program can be a different buffer than cb0 in the fragment - # program), so cbuffers are keyed by (stage, slot) — never merged + # program), so cbuffers are keyed by (stage, slot) — never merged # across stages, or different buffers that share a slot number # would collide at the same byte offsets. by_key = {} @@ -221,14 +211,14 @@ def _next_slot(): for v in b.get("m_VectorParams", []) or []: cb["variables"].append({ "name": index_to_name.get(v.get("m_NameIndex"), ""), - "offset": ShaderMetadata._parameter_offset(v), + "offset": parameter_offset(v), "dim": v.get("m_Dim", 0), "arraySize": v.get("m_ArraySize", 0), }) for m in b.get("m_MatrixParams", []) or []: cb["variables"].append({ "name": index_to_name.get(m.get("m_NameIndex"), ""), - "offset": ShaderMetadata._parameter_offset(m), + "offset": parameter_offset(m), "rowCount": m.get("m_RowCount", 0), "arraySize": m.get("m_ArraySize", 0), "isMatrix": True, @@ -236,7 +226,7 @@ def _next_slot(): # A program can bind a buffer (its name index appears in # m_ConstantBufferBindings) without listing it in - # m_ConstantBuffers — Unity only serializes the buffers the + # m_ConstantBuffers — Unity only serializes the buffers the # stage touches. When the bound name's layout is known from # another pass/stage of the same shader, reuse it so reads of # that (stage, slot) still resolve to real names. @@ -280,7 +270,7 @@ def _next_slot(): @staticmethod def _unwrap(v, default=None): # Unity wraps most scalar render state in - # {"m_Name": "...", "m_Value": N} — flatten to just N. + # {"m_Name": "...", "m_Value": N} — flatten to just N. if isinstance(v, dict) and "m_Value" in v: return v["m_Value"] return v if v is not None else default @@ -290,7 +280,7 @@ def _render_state(self, state): # like m_Culling/m_ZTest/m_ZWrite/m_RtBlend0 (values wrapped as # {"m_Value": N}). That's the schema the C# side (HlslRenderstateBuilder) # consumes. A few exports use a nested rasterState/depthState shape - # instead — fall back to that when the flat keys are absent. + # instead — fall back to that when the flat keys are absent. if not isinstance(state, dict): return {} @@ -361,4 +351,4 @@ def _tags(tags): if isinstance(entries, dict): for k, v in entries.items(): out[k] = v.get("second", v) if isinstance(v, dict) else v - return out \ No newline at end of file + return out diff --git a/libs/shader_json.py b/libs/shader_json.py new file mode 100644 index 0000000..5827511 --- /dev/null +++ b/libs/shader_json.py @@ -0,0 +1,116 @@ +"""Normalize the serialized field names used by AssetStudio and AssetRipper.""" + +import json + + +def parameter_offset(parameter): + # Unity renamed this field; AssetRipper also normalizes older exports. + field = "m_OffsetInConstantBuffer" if "m_OffsetInConstantBuffer" in parameter else "m_Index" + offset = parameter.get(field) + if type(offset) is not int or offset < 0 or offset % 4: + raise ValueError( + f"Invalid or missing constant-buffer byte offset for parameter {parameter.get('m_NameIndex')}: {offset!r}") + return offset + + +def _alias(data, canonical, legacy): + if legacy not in data: + return + value = data.pop(legacy) + if canonical in data and data[canonical] != value: + raise ValueError(f"Conflicting shader fields: {canonical} and {legacy}") + data[canonical] = value + + +def _mapping(value, label): + if isinstance(value, dict): + return value + if not isinstance(value, list): + raise ValueError(f"Invalid serialized {label} mapping") + result = {} + for entry in value: + if not isinstance(entry, dict) or set(entry) != {"Key", "Value"}: + raise ValueError(f"Invalid serialized {label} entry") + key = entry["Key"] + if not isinstance(key, str) or key in result: + raise ValueError(f"Invalid or duplicate {label} key: {key!r}") + result[key] = entry["Value"] + return result + + +def _tags(owner): + tags = owner.get("m_Tags") + if isinstance(tags, dict): + _alias(tags, "m_Tags", "tags") + if "m_Tags" in tags: + tags["m_Tags"] = _mapping(tags["m_Tags"], "tag") + + +def _wrapped(value, integer=False): + if not isinstance(value, dict): + return value + _alias(value, "m_Name", "name") + _alias(value, "m_Value", "val") + if integer and "m_Value" in value: + number = value["m_Value"] + # AssetStudio serializes these enum/mask values as floating-point JSON. + if type(number) not in (int, float) or int(number) != number: + raise ValueError(f"Invalid integer render-state value: {number!r}") + value["m_Value"] = int(number) + return value + + +def _render_state(state): + for name in ("zClip", "zTest", "zWrite", "culling", "conservative", "alphaToMask", + "stencilReadMask", "stencilWriteMask", "stencilRef", "offsetFactor", + "offsetUnits", "rtSeparateBlend", "lighting", "fogMode"): + canonical = "m_" + name[0].upper() + name[1:] + _alias(state, canonical, name) + if canonical in state: + state[canonical] = _wrapped(state[canonical], name not in ("offsetFactor", "offsetUnits")) + for index in range(8): + field = f"m_RtBlend{index}" + _alias(state, field, f"rtBlend{index}") + blend = state.get(field) + if isinstance(blend, dict): + for old, new in (("srcBlend", "SourceBlend"), ("destBlend", "DestinationBlend"), + ("srcBlendAlpha", "SourceBlendAlpha"), ("destBlendAlpha", "DestinationBlendAlpha"), + ("blendOp", "BlendOp"), ("blendOpAlpha", "BlendOpAlpha"), ("colMask", "ColMask")): + _alias(blend, "m_" + new, old) + if "m_" + new in blend: + blend["m_" + new] = _wrapped(blend["m_" + new], integer=True) + for name in ("stencilOp", "stencilOpFront", "stencilOpBack"): + field = "m_" + name[0].upper() + name[1:] + _alias(state, field, name) + stencil = state.get(field) + if isinstance(stencil, dict): + for operation in ("pass", "fail", "zFail", "comp"): + canonical = "m_" + operation[0].upper() + operation[1:] + _alias(stencil, canonical, operation) + if canonical in stencil: + stencil[canonical] = _wrapped(stencil[canonical], integer=True) + _tags(state) + + +def load_shader_json(path): + with open(path, encoding="utf-8-sig") as stream: + data = json.load(stream) + for name in ("compressedBlob", "compressedLengths", "decompressedLengths", "platforms", "offsets", "stageCounts"): + _alias(data, "m_" + name[0].upper() + name[1:], name) + parsed = data.get("m_ParsedForm", data) + # The serialized object name can be empty; the ShaderLab name is in ParsedForm. + if not data.get("m_Name"): + data["m_Name"] = parsed.get("m_Name") + for prop in parsed.get("m_PropInfo", {}).get("m_Props", []): + for index in range(4): + _alias(prop, f"m_DefValue_{index}_", f"m_DefValue[{index}]") + for subshader in parsed.get("m_SubShaders", []): + _tags(subshader) + for shader_pass in subshader.get("m_Passes", []): + if "m_NameIndices" in shader_pass: + shader_pass["m_NameIndices"] = _mapping(shader_pass["m_NameIndices"], "name-index") + for stage in ("Vertex", "Fragment", "Geometry", "Hull", "Domain", "RayTracing"): + _alias(shader_pass, "m_Prog" + stage, "prog" + stage) + _tags(shader_pass) + _render_state(shader_pass.get("m_State", {})) + return data diff --git a/tests/test_exporter_schemas.py b/tests/test_exporter_schemas.py new file mode 100644 index 0000000..092d86e --- /dev/null +++ b/tests/test_exporter_schemas.py @@ -0,0 +1,136 @@ +import base64 +import copy +import json +import tempfile +import unittest +from pathlib import Path + +import lz4.block + +from libs.blob import ShaderBlob +from libs.metadata import ShaderMetadata +from libs.shader_json import load_shader_json + + +class ExporterSchemaTests(unittest.TestCase): + def studio(self): + payload = b"generic serialized shader data" + compressed = lz4.block.compress(payload, store_size=False) + shader = { + "m_Name": "", + "compressedBlob": base64.b64encode(compressed).decode("ascii"), + "compressedLengths": [[len(compressed)]], "decompressedLengths": [[len(payload)]], + "platforms": [4], "offsets": [[0]], + "m_ParsedForm": { + "m_Name": "Test/ExporterSchemas", "m_KeywordNames": ["DETAIL"], + "m_PropInfo": {"m_Props": [{ + "m_Name": "_Tint", "m_Type": 0, + "m_DefValue[0]": 0.25, "m_DefValue[1]": 0.5, + "m_DefValue[2]": 0.75, "m_DefValue[3]": 1.0, + }]}, + "m_SubShaders": [{ + "m_Tags": {"tags": [{"Key": "RenderType", "Value": "Opaque"}]}, + "m_Passes": [{ + "m_NameIndices": [{"Key": "Globals", "Value": 1}, {"Key": "_Tint", "Value": 2}], + "progFragment": { + "m_PlayerSubPrograms": [[{"m_BlobIndex": 3, "m_GpuProgramType": 17, + "m_KeywordIndices": [0]}]], + "m_SerializedKeywordStateMask": [0], + "m_CommonParameters": { + "m_ConstantBufferBindings": [{"m_NameIndex": 1, "m_Index": 2}], + "m_ConstantBuffers": [{"m_NameIndex": 1, "m_Size": 64, + "m_VectorParams": [{"m_NameIndex": 2, "m_Index": 16, "m_Dim": 4}]}], + }, + }, + "m_State": { + "m_Tags": {"tags": [{"Key": "LIGHTMODE", "Value": "FORWARDBASE"}]}, + "culling": {"name": "", "val": 2.0}, + "zTest": {"val": 4.0}, "zWrite": {"val": 1.0}, + "offsetFactor": {"val": 0.125}, + "rtBlend0": {"srcBlend": {"val": 5.0}, "destBlend": {"val": 10.0}, + "colMask": {"val": 14.0}}, + "stencilOpFront": {"comp": {"val": 8.0}, "pass": {"val": 2.0}}, + }, + }], + }], + }, + } + return shader, payload + + def load(self, shader): + with tempfile.TemporaryDirectory() as folder: + path = Path(folder) / "shader.json" + path.write_text(json.dumps(shader), encoding="utf-8") + return load_shader_json(path), ShaderMetadata(path).build(), ShaderBlob(path).get_blob() + + def test_assetstudio_fields_keep_values_and_bindings(self): + shader, payload = self.studio() + normalized, metadata, blob = self.load(shader) + self.assertEqual(blob, payload) + self.assertEqual(normalized["m_Name"], "Test/ExporterSchemas") + self.assertEqual(metadata["properties"][0]["defaultValue"], + {"x": 0.25, "y": 0.5, "z": 0.75, "w": 1.0}) + shader_pass = metadata["subShaders"][0]["passes"][0] + self.assertEqual(shader_pass["tags"], {"LIGHTMODE": "FORWARDBASE"}) + self.assertEqual(shader_pass["constantBuffers"][0]["slot"], 2) + self.assertEqual(shader_pass["constantBuffers"][0]["variables"][0]["offset"], 16) + state = shader_pass["renderState"] + self.assertEqual((state["cull"], state["zTest"], state["zWrite"]), (2, 4, 1)) + self.assertEqual(state["offsetFactor"], 0.125) + self.assertEqual((state["blend"]["srcBlend"], state["blend"]["dstBlend"], state["blend"]["colorMask"]), (5, 10, 14)) + self.assertIs(type(state["stencilOpFront"]["m_Comp"]["m_Value"]), int) + + def test_assetripper_fields_produce_identical_inputs(self): + studio, payload = self.studio() + # Construct the alternative schema explicitly, independently of the loader. + ripper = copy.deepcopy(studio) + for old, new in (("compressedBlob", "m_CompressedBlob"), ("compressedLengths", "m_CompressedLengths"), + ("decompressedLengths", "m_DecompressedLengths"), ("platforms", "m_Platforms"), ("offsets", "m_Offsets")): + ripper[new] = ripper.pop(old) + ripper["m_Name"] = "Test/ExporterSchemas" + parsed = ripper["m_ParsedForm"] + prop = parsed["m_PropInfo"]["m_Props"][0] + for i in range(4): + prop[f"m_DefValue_{i}_"] = prop.pop(f"m_DefValue[{i}]") + subshader = parsed["m_SubShaders"][0] + subshader["m_Tags"] = {"m_Tags": {"RenderType": "Opaque"}} + shader_pass = subshader["m_Passes"][0] + shader_pass["m_NameIndices"] = {"Globals": 1, "_Tint": 2} + shader_pass["m_ProgFragment"] = shader_pass.pop("progFragment") + parameter = shader_pass["m_ProgFragment"]["m_CommonParameters"]["m_ConstantBuffers"][0]["m_VectorParams"][0] + parameter["m_OffsetInConstantBuffer"] = parameter.pop("m_Index") + shader_pass["m_State"] = { + "m_Tags": {"m_Tags": {"LIGHTMODE": "FORWARDBASE"}}, + "m_Culling": {"m_Name": "", "m_Value": 2}, + "m_ZTest": {"m_Value": 4}, "m_ZWrite": {"m_Value": 1}, + "m_OffsetFactor": {"m_Value": 0.125}, + "m_RtBlend0": {"m_SourceBlend": {"m_Value": 5}, "m_DestinationBlend": {"m_Value": 10}, "m_ColMask": {"m_Value": 14}}, + "m_StencilOpFront": {"m_Comp": {"m_Value": 8}, "m_Pass": {"m_Value": 2}}, + } + _, expected, _ = self.load(studio) + _, actual, blob = self.load(ripper) + self.assertEqual(actual, expected) + self.assertEqual(blob, payload) + + def test_conflicting_aliases_fail(self): + shader, _ = self.studio() + shader["m_CompressedLengths"] = [[1]] + with self.assertRaisesRegex(ValueError, "Conflicting shader fields"): + self.load(shader) + + def test_duplicate_serialized_names_fail(self): + shader, _ = self.studio() + names = shader["m_ParsedForm"]["m_SubShaders"][0]["m_Passes"][0]["m_NameIndices"] + names.append({"Key": "Globals", "Value": 3}) + with self.assertRaisesRegex(ValueError, "duplicate"): + self.load(shader) + + def test_fractional_render_state_enum_fails(self): + shader, _ = self.studio() + shader["m_ParsedForm"]["m_SubShaders"][0]["m_Passes"][0]["m_State"]["culling"]["val"] = 1.5 + with self.assertRaisesRegex(ValueError, "integer render-state"): + self.load(shader) + + +if __name__ == "__main__": + unittest.main() From a46e588a3d4e361aabf4733fdc2d9b973c94a9d4 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 12:21:24 +0100 Subject: [PATCH 11/93] Keep explicit zero texture LOD explicit --- .../IRShaderPatternRecognition.cs | 6 +- .../IRTexturePatternRecognition.cs | 71 ------------------- Shader Decompiler/Output/HOLO_Holo.shader | 14 ++-- docs/pr-explicit-texture-lod.md | 7 ++ .../ExplicitTextureLodTests.cs | 65 +++++++++++++++++ 5 files changed, 81 insertions(+), 82 deletions(-) delete mode 100644 Shader Decompiler/Core/Optimizations/IRTexturePatternRecognition.cs create mode 100644 docs/pr-explicit-texture-lod.md create mode 100644 tests/UnityShaderDecompiler.Tests/ExplicitTextureLodTests.cs diff --git a/Shader Decompiler/Core/Optimizations/IRShaderPatternRecognition.cs b/Shader Decompiler/Core/Optimizations/IRShaderPatternRecognition.cs index 3748df9..15e4178 100644 --- a/Shader Decompiler/Core/Optimizations/IRShaderPatternRecognition.cs +++ b/Shader Decompiler/Core/Optimizations/IRShaderPatternRecognition.cs @@ -12,9 +12,8 @@ namespace Parser.Core.Optimizations; // Order matters: vector recognition folds row·vector dot-product chains // (and, separately, normalize()) first, which is what gives matrix // recognition the DotProductExpression nodes it groups into -// MatrixVectorMultiplyExpression. Texture recognition is independent of -// both and just tags along in the same fixed-point sweep. Loops to a -// fixed point because folding a row can occasionally simplify a +// MatrixVectorMultiplyExpression. Loops to a fixed point because folding +// a row can occasionally simplify a // neighboring statement enough to unblock the next match (e.g. once dead // partial-sum temporaries drop out, two previously non-adjacent // statements become adjacent). @@ -37,7 +36,6 @@ public static bool Run(List blocks) changed = false; changed |= IRVectorPatternRecognition.Run(blocks); changed |= IRMatrixPatternRecognition.Run(blocks); - changed |= IRTexturePatternRecognition.Run(blocks); changedOverall |= changed; } diff --git a/Shader Decompiler/Core/Optimizations/IRTexturePatternRecognition.cs b/Shader Decompiler/Core/Optimizations/IRTexturePatternRecognition.cs deleted file mode 100644 index 3293606..0000000 --- a/Shader Decompiler/Core/Optimizations/IRTexturePatternRecognition.cs +++ /dev/null @@ -1,71 +0,0 @@ -using Parser.DXBC.IR; - -namespace Parser.Core.Optimizations; - -// Post-SSA, pre-HLSL-generation pass (roadmap: "Texture recognition"). -// TextureOperationExpression already models every sample/load/gather -// opcode faithfully as parsed, so there's no restructuring to do the way -// there is for matrix/vector patterns — the one real simplification -// available at the IR level is collapsing an explicit sample-at-LOD-0 -// into a plain Sample, since that's the shape a backend would otherwise -// have to special-case anyway: -// -// SampleLevel(coord, lod=0) -> Sample(coord) -// -// (SampleLevel with a non-zero or non-constant LOD stays as-is — that's -// a genuine explicit-LOD sample, e.g. Texture2D.SampleLevel(), not a -// plain tex2D()/Texture2D.Sample().) -public static class IRTexturePatternRecognition -{ - public static bool Run(List blocks) - { - bool changed = false; - - foreach (IRBlock block in blocks) - { - for (int i = 0; i < block.Statements.Count; i++) - { - IRStatement rewritten = IRStatementRewriter.RewriteExpressions(block.Statements[i], Recognize); - - if (rewritten.ToString() != block.Statements[i].ToString()) - { - block.Statements[i] = rewritten; - changed = true; - } - } - } - - return changed; - } - - private static IRExpression Recognize(IRExpression expr) - { - if (expr is not IRExpression.TextureOperationExpression - { - Operation: IRExpression.TextureOperation.SampleLevel, - LOD: IRExpression.ConstantExpression lod, - } tex) - { - return expr; - } - - if (!IRConstantMath.IsAllComponents(lod, 0)) - return expr; - - return new IRExpression.TextureOperationExpression - { - Operation = IRExpression.TextureOperation.Sample, - Resource = tex.Resource, - Sampler = tex.Sampler, - Coordinates = tex.Coordinates, - Offset = tex.Offset, - // LOD intentionally dropped: Sample has no LOD field — that's - // the whole point of this rewrite. - Bias = tex.Bias, - CompareValue = tex.CompareValue, - GradX = tex.GradX, - GradY = tex.GradY, - SampleIndex = tex.SampleIndex, - }; - } -} \ No newline at end of file diff --git a/Shader Decompiler/Output/HOLO_Holo.shader b/Shader Decompiler/Output/HOLO_Holo.shader index 95947ba..83dd427 100644 --- a/Shader Decompiler/Output/HOLO_Holo.shader +++ b/Shader Decompiler/Output/HOLO_Holo.shader @@ -135,12 +135,12 @@ Shader "HOLO/Holo" float4 uvMMap_xyzw_2 = mad(i.texcoord0.xyxx, r1_w_1.xxxx, r0_w_4.xxxx); float uvMMap_x_2 = uvMMap_xyzw_2.x; float uvMMap_y_1 = uvMMap_xyzw_2.y; - float4 sampleMMap_xyzw_3 = _MainTex.SampleLevel(sampler_MainTex, (float4(uvMMap_x_2, uvMMap_y_1, uvMMap_x_2, uvMMap_x_2)).xy, 0); + float4 r4_xyzw_3 = _MainTex.SampleLevel(sampler_MainTex, (float4(uvMMap_x_2, uvMMap_y_1, uvMMap_x_2, uvMMap_x_2)).xy, 0); float r0_w_5 = (int)(_noise_details); - float4 r4_xyzw_3 = (_Time.xxyz * float4(0, 0.1, 0.1, 0.1)); - float r4_y_3 = r4_xyzw_3.y; - float r4_z_2 = r4_xyzw_3.z; - float r4_w_2 = r4_xyzw_3.w; + float r4_y_3 = ((_Time.xxyz * float4(0, 0.1, 0.1, 0.1))).y; + float4 r4_xyzw_2 = (_Time.xxyz * float4(0, 0.1, 0.1, 0.1)); + float r4_z_2 = r4_xyzw_2.z; + float r4_w_2 = r4_xyzw_2.w; float TEXCOORD0_x_2 = (i.texcoord0.xyxx).x; float TEXCOORD0_y_1 = (i.texcoord0.xyxx).y; float r5_z_1 = 0; @@ -184,8 +184,8 @@ Shader "HOLO/Holo" float r4_y_5 = r4_xyzw_5.y; float r4_z_4 = r4_xyzw_5.z; float r4_w_3 = r4_xyzw_5.w; - float r4_x_4 = ((sampleMMap_xyzw_3.xxxx * float4(r4_y_5, r4_z_4, r4_w_3, r4_y_5))).x; - float4 r4_xyzw_6 = (sampleMMap_xyzw_3.xxxx * float4(r4_y_5, r4_z_4, r4_w_3, r4_y_5)); + float r4_x_4 = ((r4_xyzw_3.xxxx * float4(r4_y_5, r4_z_4, r4_w_3, r4_y_5))).x; + float4 r4_xyzw_6 = (r4_xyzw_3.xxxx * float4(r4_y_5, r4_z_4, r4_w_3, r4_y_5)); float r4_y_6 = r4_xyzw_6.y; float r4_z_5 = r4_xyzw_6.z; float r4_x_5 = (r4_x_4 * _X); diff --git a/docs/pr-explicit-texture-lod.md b/docs/pr-explicit-texture-lod.md new file mode 100644 index 0000000..e68415c --- /dev/null +++ b/docs/pr-explicit-texture-lod.md @@ -0,0 +1,7 @@ +# Keep explicit zero texture LOD explicit + +Do not replace SampleLevel(..., 0) with Sample(...): implicit derivatives can select a different mip. Remove the transformation and add a regression checking that the explicit LOD survives optimization. + +Validation: 52 C# tests and all four existing PicaVoxel extraction/decompilation tests pass. C# tests use an en-US default culture; the upstream fr-FR golden-output failure is addressed independently by the numeric-formatting PR. The golden output was regenerated by the emitter for this change only; existing comparison assertions are unchanged. + +This branch is based directly on upstream `1850608`; it does not include the other review branches. No proprietary fixtures or machine-specific paths are added. diff --git a/tests/UnityShaderDecompiler.Tests/ExplicitTextureLodTests.cs b/tests/UnityShaderDecompiler.Tests/ExplicitTextureLodTests.cs new file mode 100644 index 0000000..036097d --- /dev/null +++ b/tests/UnityShaderDecompiler.Tests/ExplicitTextureLodTests.cs @@ -0,0 +1,65 @@ +using System; +using System.Linq; +using Parser.Core.Hlsl; +using Parser.Core.Hlsl.Ast; +using Parser.Core.Analysis; +using Parser.Core.Optimizations; +using Parser.DXBC.Instructions; +using Parser.DXBC.IR; +using Xunit; + +namespace UnityShaderDecompiler.Tests; + +public sealed class ExplicitTextureLodTests +{ + [Fact] + public void ExplicitZeroLodIsNotReplacedByImplicitDerivatives() + { + IRExpression.TextureOperationExpression sample = new IRExpression.TextureOperationExpression + { + Operation = IRExpression.TextureOperation.SampleLevel, + LOD = Float(0), Coordinates = Float(1), + Resource = new IRRegister { RegisterType = RegisterType.Resource, Index = 0 }, + Sampler = new IRRegister { RegisterType = RegisterType.Sampler, Index = 0 }, + }; + IRBlock block = new IRBlock(); + block.Statements.Add(new IRStatement.IRAssignment { Destination = Temp(0), Expression = sample }); + + IRShaderPatternRecognition.Run(new() { block }); + + IRStatement.IRAssignment assignment = Assert.IsType(block.Statements[0]); + IRExpression.TextureOperationExpression actual = Assert.IsType(assignment.Expression); + Assert.Equal(IRExpression.TextureOperation.SampleLevel, actual.Operation); + Assert.Same(sample.LOD, actual.LOD); + } + + private static string Print(HlslFunctionNode function) + { + HlslShaderNode shader = new HlslShaderNode { Name = "Test/Scope" }; + HlslSubShaderNode subShader = new HlslSubShaderNode(); + subShader.Passes.Add(new HlslPassNode { FragmentFunction = function }); + shader.SubShaders.Add(subShader); + return HlslPrettyPrinter.Print(shader, fuseTemps: false); + } + + private static IRRegister Temp(uint index, byte mask = 1) + { + IRRegister register = new IRRegister + { + RegisterType = RegisterType.Temp, + Index = index, + Type = IRValueType.Float, + Mask = mask, + ComponentMode = Operand.OperandComponentMode.Mask, + }; + for (int component = 0; component < 4; component++) + if ((mask & (1 << component)) != 0) + register.SsaVersion[component] = 1; + return register; + } + + private static IRExpression.ConstantExpression Float(float value) => new() + { + RawValues = new[] { unchecked((uint)BitConverter.SingleToInt32Bits(value)) }, + }; +} From 54feced342c4958d5a4c78bfb5d6172cef91d2f5 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 12:22:19 +0100 Subject: [PATCH 12/93] Keep temporary register identities distinct --- .../Core/Hlsl/HlslPrettyPrinter.cs | 7 +- Shader Decompiler/Output/HOLO_Holo.shader | 152 +++++++++--------- docs/pr-temporary-register-names.md | 7 + .../TemporaryRegisterNamesTests.cs | 73 +++++++++ 4 files changed, 162 insertions(+), 77 deletions(-) create mode 100644 docs/pr-temporary-register-names.md create mode 100644 tests/UnityShaderDecompiler.Tests/TemporaryRegisterNamesTests.cs diff --git a/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs b/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs index 222df18..8b585bb 100644 --- a/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs +++ b/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs @@ -1607,7 +1607,12 @@ private static List MaskIndices(byte mask) private static string BaseIdentifier(IRRegister reg, PrintContext ctx) { if (reg.SymbolicName is not null) - return reg.SymbolicName; + return reg.RegisterType switch + { + RegisterType.Temp => $"{reg.SymbolicName}_r{reg.Index}", + RegisterType.IndexableTemp => $"{reg.SymbolicName}_x{reg.Index}", + _ => reg.SymbolicName, + }; return reg.RegisterType switch { diff --git a/Shader Decompiler/Output/HOLO_Holo.shader b/Shader Decompiler/Output/HOLO_Holo.shader index 95947ba..bf6518a 100644 --- a/Shader Decompiler/Output/HOLO_Holo.shader +++ b/Shader Decompiler/Output/HOLO_Holo.shader @@ -126,32 +126,32 @@ Shader "HOLO/Holo" program1Output vert(program1Input i) { program1Output o = (program1Output)0; - float4x4 objectToView = mul(unity_ObjectToWorld, unity_MatrixV); + float4x4 objectToView0_r = mul(unity_ObjectToWorld, unity_MatrixV); float r0_w_1 = (_Speed * _Time.x); float r0_w_2 = (r0_w_1 * 60); float r0_w_3 = (sin(r0_w_2) + 1); float r1_w_1 = (cos(r0_w_2) * _M_map_ST.y); float r0_w_4 = (r0_w_3 * _M_map_ST.w); - float4 uvMMap_xyzw_2 = mad(i.texcoord0.xyxx, r1_w_1.xxxx, r0_w_4.xxxx); - float uvMMap_x_2 = uvMMap_xyzw_2.x; - float uvMMap_y_1 = uvMMap_xyzw_2.y; - float4 sampleMMap_xyzw_3 = _MainTex.SampleLevel(sampler_MainTex, (float4(uvMMap_x_2, uvMMap_y_1, uvMMap_x_2, uvMMap_x_2)).xy, 0); + float4 uvMMap_r4_xyzw_2 = mad(i.texcoord0.xyxx, r1_w_1.xxxx, r0_w_4.xxxx); + float uvMMap_r4_x_2 = uvMMap_r4_xyzw_2.x; + float uvMMap_r4_y_1 = uvMMap_r4_xyzw_2.y; + float4 sampleMMap_r4_xyzw_3 = _MainTex.SampleLevel(sampler_MainTex, (float4(uvMMap_r4_x_2, uvMMap_r4_y_1, uvMMap_r4_x_2, uvMMap_r4_x_2)).xy, 0); float r0_w_5 = (int)(_noise_details); float4 r4_xyzw_3 = (_Time.xxyz * float4(0, 0.1, 0.1, 0.1)); float r4_y_3 = r4_xyzw_3.y; float r4_z_2 = r4_xyzw_3.z; float r4_w_2 = r4_xyzw_3.w; - float TEXCOORD0_x_2 = (i.texcoord0.xyxx).x; - float TEXCOORD0_y_1 = (i.texcoord0.xyxx).y; + float TEXCOORD0_r5_x_2 = (i.texcoord0.xyxx).x; + float TEXCOORD0_r5_y_1 = (i.texcoord0.xyxx).y; float r5_z_1 = 0; float r1_w_2 = r0_w_5; - float3 noiseAccum_xyz_2 = (float4(0, 0, 0, 0)).xyz; - float2 r5_yz_2 = float2(TEXCOORD0_y_1, r5_z_1); + float3 noiseAccum_r6_xyz_2 = (float4(0, 0, 0, 0)).xyz; + float2 r5_yz_2 = float2(TEXCOORD0_r5_y_1, r5_z_1); float r1_w_3 = r1_w_2; - float r5_x_3 = TEXCOORD0_x_2; + float r5_x_3 = TEXCOORD0_r5_x_2; float r2_w_1; float3 r7_xyz_1; - float3 noiseAccum_xyz_6; + float3 noiseAccum_r6_xyz_6; float r5_x_8; float r5_y_7; float r5_z_7; @@ -162,13 +162,13 @@ Shader "HOLO/Holo" r2_w_1 = (0 >= r1_w_3); if (r2_w_1) break; r7_xyz_1 = (floor(float4(r5_x_3, r5_yz_2.x, r5_yz_2.y, r5_x_3))).xyz; - noiseAccum_xyz_6 = ((noiseAccum_xyz_2.xyzx + (sin((mad(r7_xyz_1.xxzx, r7_xyz_1.yyyy, ((float4(r4_y_3, r4_z_2, r4_w_2, r4_y_3) * r7_xyz_1.xyzx)).xyzx)).xyzx)).xyzx)).xyz; + noiseAccum_r6_xyz_6 = ((noiseAccum_r6_xyz_2.xyzx + (sin((mad(r7_xyz_1.xxzx, r7_xyz_1.yyyy, ((float4(r4_y_3, r4_z_2, r4_w_2, r4_y_3) * r7_xyz_1.xyzx)).xyzx)).xyzx)).xyzx)).xyz; float4 r5_xyzw_8 = (float4(r5_x_3, r5_yz_2.x, r5_yz_2.y, r5_x_3) * float4(2.5, 2.5, 2.5, 0)); r5_x_8 = r5_xyzw_8.x; r5_y_7 = r5_xyzw_8.y; r5_z_7 = r5_xyzw_8.z; r1_w_4 = (r1_w_3 + -1); - noiseAccum_xyz_2 = noiseAccum_xyz_6; + noiseAccum_r6_xyz_2 = noiseAccum_r6_xyz_6; r5_yz_2 = float2(r5_y_7, r5_z_7); r1_w_3 = r1_w_4; r5_x_3 = r5_x_8; @@ -176,7 +176,7 @@ Shader "HOLO/Holo" float4 r4_xyzw_4 = (cb0_values[7].yyxy * float4(0, 0.001, 0.1, 0)); float r4_y_4 = r4_xyzw_4.y; float r4_z_3 = r4_xyzw_4.z; - float4 r5_xyzw_4 = (r4_y_4.xxxx * noiseAccum_xyz_2.xyzx); + float4 r5_xyzw_4 = (r4_y_4.xxxx * noiseAccum_r6_xyz_2.xyzx); float r5_x_4 = r5_xyzw_4.x; float r5_y_3 = r5_xyzw_4.y; float r5_z_3 = r5_xyzw_4.z; @@ -184,8 +184,8 @@ Shader "HOLO/Holo" float r4_y_5 = r4_xyzw_5.y; float r4_z_4 = r4_xyzw_5.z; float r4_w_3 = r4_xyzw_5.w; - float r4_x_4 = ((sampleMMap_xyzw_3.xxxx * float4(r4_y_5, r4_z_4, r4_w_3, r4_y_5))).x; - float4 r4_xyzw_6 = (sampleMMap_xyzw_3.xxxx * float4(r4_y_5, r4_z_4, r4_w_3, r4_y_5)); + float r4_x_4 = ((sampleMMap_r4_xyzw_3.xxxx * float4(r4_y_5, r4_z_4, r4_w_3, r4_y_5))).x; + float4 r4_xyzw_6 = (sampleMMap_r4_xyzw_3.xxxx * float4(r4_y_5, r4_z_4, r4_w_3, r4_y_5)); float r4_y_6 = r4_xyzw_6.y; float r4_z_5 = r4_xyzw_6.z; float r4_x_5 = (r4_x_4 * _X); @@ -210,41 +210,41 @@ Shader "HOLO/Holo" float4 r6_xyzw_4 = mad(unity_MatrixVP[0], r5_xyzw_7.xxxx, r6_xyzw_3); float4 r6_xyzw_5 = mad(unity_MatrixVP[2], r5_xyzw_7.zzzz, r6_xyzw_4); o.sv_Position0.xyzw = mad(unity_MatrixVP[3], r5_xyzw_7.wwww, r6_xyzw_5); - float4 worldPos_xyzw_9 = mad(unity_ObjectToWorld[3].xyzx, i.position0.wwww, float4(r4_x_8, r4_y_10, r4_z_8, r4_x_8)); - float worldPos_x_9 = worldPos_xyzw_9.x; - float worldPos_y_11 = worldPos_xyzw_9.y; - float worldPos_z_9 = worldPos_xyzw_9.z; - float3 viewNormal_xyz_5 = (mad(objectToView[0].xyz.xyzx, i.normal0.xxxx, ((objectToView[1].xyz.xyzx * i.normal0.yyyy)).xyzx)).xyz; - float3 viewNormal_xyz_6 = (mad(objectToView[2].xyz.xyzx, i.normal0.zzzz, viewNormal_xyz_5.xyzx)).xyz; - float3 viewNormal_xyz_7 = (mad(objectToView[3].xyz.xyzx, i.normal0.wwww, viewNormal_xyz_6.xyzx)).xyz; - float2 unitViewNormal_xy_8 = normalize(viewNormal_xyz_7); - float worldNormal_x_6 = dot(i.normal0.xyzx, unity_WorldToObject[0].xyzx); - float worldNormal_y_6 = dot(i.normal0.xyzx, unity_WorldToObject[1].xyzx); - float worldNormal_z_6 = dot(i.normal0.xyzx, unity_WorldToObject[2].xyzx); - float r0_z_10 = dot(float4(worldNormal_x_6, worldNormal_y_6, worldNormal_z_6, worldNormal_x_6), float4(worldNormal_x_6, worldNormal_y_6, worldNormal_z_6, worldNormal_x_6)); + float4 worldPos_r4_xyzw_9 = mad(unity_ObjectToWorld[3].xyzx, i.position0.wwww, float4(r4_x_8, r4_y_10, r4_z_8, r4_x_8)); + float worldPos_r4_x_9 = worldPos_r4_xyzw_9.x; + float worldPos_r4_y_11 = worldPos_r4_xyzw_9.y; + float worldPos_r4_z_9 = worldPos_r4_xyzw_9.z; + float3 viewNormal_r0_xyz_5 = (mad(objectToView0_r[0].xyz.xyzx, i.normal0.xxxx, ((objectToView0_r[1].xyz.xyzx * i.normal0.yyyy)).xyzx)).xyz; + float3 viewNormal_r0_xyz_6 = (mad(objectToView0_r[2].xyz.xyzx, i.normal0.zzzz, viewNormal_r0_xyz_5.xyzx)).xyz; + float3 viewNormal_r0_xyz_7 = (mad(objectToView0_r[3].xyz.xyzx, i.normal0.wwww, viewNormal_r0_xyz_6.xyzx)).xyz; + float2 unitViewNormal_r0_xy_8 = normalize(viewNormal_r0_xyz_7); + float worldNormal_r1_x_6 = dot(i.normal0.xyzx, unity_WorldToObject[0].xyzx); + float worldNormal_r1_y_6 = dot(i.normal0.xyzx, unity_WorldToObject[1].xyzx); + float worldNormal_r1_z_6 = dot(i.normal0.xyzx, unity_WorldToObject[2].xyzx); + float r0_z_10 = dot(float4(worldNormal_r1_x_6, worldNormal_r1_y_6, worldNormal_r1_z_6, worldNormal_r1_x_6), float4(worldNormal_r1_x_6, worldNormal_r1_y_6, worldNormal_r1_z_6, worldNormal_r1_x_6)); float r0_z_11 = rsqrt(r0_z_10); - float3 unitWorldNormal_xyz_7 = ((r0_z_11.xxxx * float4(worldNormal_x_6, worldNormal_y_6, worldNormal_z_6, worldNormal_x_6))).xyz; - o.texcoord2.xy = (mad(unitViewNormal_xy_8.xyxx, float4(0.5, 0.5, 0, 0), float4(0.5, 0.5, 0, 0))).xy; - float4 dirToSurface_xyzw_9 = (float4(worldPos_x_9, worldPos_y_11, worldPos_z_9, worldPos_x_9) + -_WorldSpaceCameraPos.xyzx); - float dirToSurface_x_9 = dirToSurface_xyzw_9.x; - float dirToSurface_y_9 = dirToSurface_xyzw_9.y; - float dirToSurface_z_12 = dirToSurface_xyzw_9.z; - float r0_w_6 = dot(float4(dirToSurface_x_9, dirToSurface_y_9, dirToSurface_z_12, dirToSurface_x_9), float4(dirToSurface_x_9, dirToSurface_y_9, dirToSurface_z_12, dirToSurface_x_9)); + float3 unitWorldNormal_r1_xyz_7 = ((r0_z_11.xxxx * float4(worldNormal_r1_x_6, worldNormal_r1_y_6, worldNormal_r1_z_6, worldNormal_r1_x_6))).xyz; + o.texcoord2.xy = (mad(unitViewNormal_r0_xy_8.xyxx, float4(0.5, 0.5, 0, 0), float4(0.5, 0.5, 0, 0))).xy; + float4 dirToSurface_r0_xyzw_9 = (float4(worldPos_r4_x_9, worldPos_r4_y_11, worldPos_r4_z_9, worldPos_r4_x_9) + -_WorldSpaceCameraPos.xyzx); + float dirToSurface_r0_x_9 = dirToSurface_r0_xyzw_9.x; + float dirToSurface_r0_y_9 = dirToSurface_r0_xyzw_9.y; + float dirToSurface_r0_z_12 = dirToSurface_r0_xyzw_9.z; + float r0_w_6 = dot(float4(dirToSurface_r0_x_9, dirToSurface_r0_y_9, dirToSurface_r0_z_12, dirToSurface_r0_x_9), float4(dirToSurface_r0_x_9, dirToSurface_r0_y_9, dirToSurface_r0_z_12, dirToSurface_r0_x_9)); float r0_w_7 = rsqrt(r0_w_6); - float4 unitDirToSurface_xyzw_10 = (r0_w_7.xxxx * float4(dirToSurface_x_9, dirToSurface_y_9, dirToSurface_z_12, dirToSurface_x_9)); - float unitDirToSurface_x_10 = unitDirToSurface_xyzw_10.x; - float unitDirToSurface_y_10 = unitDirToSurface_xyzw_10.y; - float unitDirToSurface_z_13 = unitDirToSurface_xyzw_10.z; - float nDotV_x_11 = dot(float4(unitDirToSurface_x_10, unitDirToSurface_y_10, unitDirToSurface_z_13, unitDirToSurface_x_10), unitWorldNormal_xyz_7.xyzx); + float4 unitDirToSurface_r0_xyzw_10 = (r0_w_7.xxxx * float4(dirToSurface_r0_x_9, dirToSurface_r0_y_9, dirToSurface_r0_z_12, dirToSurface_r0_x_9)); + float unitDirToSurface_r0_x_10 = unitDirToSurface_r0_xyzw_10.x; + float unitDirToSurface_r0_y_10 = unitDirToSurface_r0_xyzw_10.y; + float unitDirToSurface_r0_z_13 = unitDirToSurface_r0_xyzw_10.z; + float nDotV_r0_x_11 = dot(float4(unitDirToSurface_r0_x_10, unitDirToSurface_r0_y_10, unitDirToSurface_r0_z_13, unitDirToSurface_r0_x_10), unitWorldNormal_r1_xyz_7.xyzx); float r0_y_11 = (_Scale * _Bias); - float fresnel_x_15 = pow((nDotV_x_11 + _t), _Power); - o.texcoord4.x = (fresnel_x_15 * r0_y_11); + float fresnel_r0_x_15 = pow((nDotV_r0_x_11 + _t), _Power); + o.texcoord4.x = (fresnel_r0_x_15 * r0_y_11); o.texcoord0.xyzw = i.texcoord0.xyzw; o.texcoord1.xyzw = float4(0, 0, 0, 0); o.texcoord4.yzw = (float4(0, 0, 0, 0)).yzw; o.normal0.xyzw = float4(0, 0, 0, 0); o.texcoord2.z = 0; - o.texcoord3.xyz = unitWorldNormal_xyz_7.xyz; + o.texcoord3.xyz = unitWorldNormal_r1_xyz_7.xyz; return o; } #pragma fragment frag @@ -258,35 +258,35 @@ Shader "HOLO/Holo" float r1_x_1 = sin(r0_y_1); float r0_y_2 = (r1_x_1 + 1); float2 r1_yz_1 = ((r0_x_2.xxxx * _N_map_ST.zzwz)).yz; - float2 uvNMap_yz_2 = (mad(i.texcoord0.xxyx, _N_map_ST.xxyx, r1_yz_1.xxyx)).yz; - float4 sampleNMap_xyzw_1 = _MainTex.Sample(s2, (uvNMap_yz_2.xyxx).xy); + float2 uvNMap_r1_yz_2 = (mad(i.texcoord0.xxyx, _N_map_ST.xxyx, r1_yz_1.xxyx)).yz; + float4 sampleNMap_r3_xyzw_1 = _MainTex.Sample(s2, (uvNMap_r1_yz_2.xyxx).xy); float r0_w_1 = (cos(r0_y_1) * _M_map_ST.y); float r0_y_3 = (r0_y_2 * _M_map_ST.w); - float4 uvMMap_xyzw_4 = mad(i.texcoord0.xxxy, r0_w_1.xxxx, r0_y_3.xxxx); - float uvMMap_y_4 = uvMMap_xyzw_4.y; - float uvMMap_w_2 = uvMMap_xyzw_4.w; - float4 sampleMMap_xyzw_2 = _originalDiffuse.Sample(sampler_originalDiffuse, (float4(uvMMap_y_4, uvMMap_w_2, uvMMap_y_4, uvMMap_y_4)).xy); + float4 uvMMap_r0_xyzw_4 = mad(i.texcoord0.xxxy, r0_w_1.xxxx, r0_y_3.xxxx); + float uvMMap_r0_y_4 = uvMMap_r0_xyzw_4.y; + float uvMMap_r0_w_2 = uvMMap_r0_xyzw_4.w; + float4 sampleMMap_r2_xyzw_2 = _originalDiffuse.Sample(sampler_originalDiffuse, (float4(uvMMap_r0_y_4, uvMMap_r0_w_2, uvMMap_r0_y_4, uvMMap_r0_y_4)).xy); float4 r0_xyzw_5 = (_OriginalUVSwitch.xxxx == float4(0, 0, 0, 1)); float r0_y_5 = r0_xyzw_5.y; float r0_w_3 = r0_xyzw_5.w; - float4 sampleDiffuse_xyzw_3; + float4 sampleDiffuse_r4_xyzw_3; if (r0_y_5) { - float2 uvDiffuse_yz_3 = (mad(i.texcoord2.xxyx, _Diffuse_ST.yyyy, _Diffuse_ST.wwww)).yz; - float4 sampleDiffuse_xyzw_2 = _Diffuse.Sample(sampler_Diffuse, (uvDiffuse_yz_3.xyxx).xy); - sampleDiffuse_xyzw_3 = sampleDiffuse_xyzw_2; + float2 uvDiffuse_r1_yz_3 = (mad(i.texcoord2.xxyx, _Diffuse_ST.yyyy, _Diffuse_ST.wwww)).yz; + float4 sampleDiffuse_r4_xyzw_2 = _Diffuse.Sample(sampler_Diffuse, (uvDiffuse_r1_yz_3.xyxx).xy); + sampleDiffuse_r4_xyzw_3 = sampleDiffuse_r4_xyzw_2; } else { - sampleDiffuse_xyzw_3 = float4(0, 0, 0, 0); + sampleDiffuse_r4_xyzw_3 = float4(0, 0, 0, 0); } - float4 sampleDiffuse_xyzw_5 = sampleDiffuse_xyzw_3; + float4 sampleDiffuse_r4_xyzw_5 = sampleDiffuse_r4_xyzw_3; if (r0_w_3) { float4 r4_xyzw_4 = _N_map.Sample(sampler_MainTex, (i.texcoord0.xyxx).xy); - sampleDiffuse_xyzw_5 = r4_xyzw_4; + sampleDiffuse_r4_xyzw_5 = r4_xyzw_4; } - float r0_y_6 = (sampleMMap_xyzw_2.x * sampleNMap_xyzw_1.x); + float r0_y_6 = (sampleMMap_r2_xyzw_2.x * sampleNMap_r3_xyzw_1.x); float4 r0_xyzw_3 = sin(float4(r0_x_2, r0_x_2, r0_z_1, r0_x_2)); float r0_x_3 = r0_xyzw_3.x; float r0_z_2 = r0_xyzw_3.z; @@ -303,7 +303,7 @@ Shader "HOLO/Holo" float r1_w_1 = r0_w_5; float r1_w_2 = r1_w_1; float2 r2_zw_3 = r2_zw_2.xy; - float2 noiseAccum_xy_3 = (float4(0, 0, 0, 0)).xy; + float2 noiseAccum_r3_xy_3 = (float4(0, 0, 0, 0)).xy; float r3_z_3; float r3_z_4; float r3_w_3; @@ -311,7 +311,7 @@ Shader "HOLO/Holo" float r3_w_4; float r3_z_6; float r3_w_5; - float2 noiseAccum_xy_4; + float2 noiseAccum_r3_xy_4; float2 r2_zw_4; float r1_w_5; [loop] @@ -328,14 +328,14 @@ Shader "HOLO/Holo" float4 r3_xyzw_6 = sin(float4(r3_z_5, r3_z_5, r3_z_5, r3_w_4)); r3_z_6 = r3_xyzw_6.z; r3_w_5 = r3_xyzw_6.w; - noiseAccum_xy_4 = ((float4(r3_z_6, r3_w_5, r3_z_6, r3_z_6) + noiseAccum_xy_3.xyxx)).xy; + noiseAccum_r3_xy_4 = ((float4(r3_z_6, r3_w_5, r3_z_6, r3_z_6) + noiseAccum_r3_xy_3.xyxx)).xy; r2_zw_4 = ((r2_zw_3.xxxy * float4(0, 0, 2.5, 2.5))).zw; r1_w_5 = (r1_w_2 + -1); r1_w_2 = r1_w_5; r2_zw_3 = r2_zw_4.xy; - noiseAccum_xy_3 = noiseAccum_xy_4; + noiseAccum_r3_xy_3 = noiseAccum_r3_xy_4; } - float2 r1_yz_6 = ((noiseAccum_xy_3.xxyx * _glitchColor.xxxx)).yz; + float2 r1_yz_6 = ((noiseAccum_r3_xy_3.xxyx * _glitchColor.xxxx)).yz; float r0_x_5 = ((float4(r0_x_4, r0_y_6, r0_z_3, r0_x_4) * float4(r1_yz_6.x, r1_x_1, r1_yz_6.x, r1_yz_6.x))).x; float4 r0_xyzw_7 = (float4(r0_x_4, r0_y_6, r0_z_3, r0_x_4) * float4(r1_yz_6.x, r1_x_1, r1_yz_6.x, r1_yz_6.x)); float r0_y_7 = r0_xyzw_7.y; @@ -349,33 +349,33 @@ Shader "HOLO/Holo" float4 r1_xyzw_2 = mad(float4(r0_x_6, r0_x_6, r0_z_5, r0_x_6), float4(0.1875, 0, 0.75, 0), _Color.xxzx); float r1_x_2 = r1_xyzw_2.x; float r1_z_7 = r1_xyzw_2.z; - float r4_w_6 = (sampleDiffuse_xyzw_5.w * _Opacity); - float r0_x_7 = ((float4(sampleDiffuse_xyzw_5.x, sampleDiffuse_xyzw_5.y, sampleDiffuse_xyzw_5.z, r4_w_6) * _diff_Color)).x; - float4 r0_xyzw_10 = (float4(sampleDiffuse_xyzw_5.x, sampleDiffuse_xyzw_5.y, sampleDiffuse_xyzw_5.z, r4_w_6) * _diff_Color); + float r4_w_6 = (sampleDiffuse_r4_xyzw_5.w * _Opacity); + float r0_x_7 = ((float4(sampleDiffuse_r4_xyzw_5.x, sampleDiffuse_r4_xyzw_5.y, sampleDiffuse_r4_xyzw_5.z, r4_w_6) * _diff_Color)).x; + float4 r0_xyzw_10 = (float4(sampleDiffuse_r4_xyzw_5.x, sampleDiffuse_r4_xyzw_5.y, sampleDiffuse_r4_xyzw_5.z, r4_w_6) * _diff_Color); float r0_y_10 = r0_xyzw_10.y; float r0_z_6 = r0_xyzw_10.z; float r0_w_6 = r0_xyzw_10.w; float r1_w_3 = (_monochrom == 1); float r2_x_5 = (r1_z_7 + (r1_y_7 + r1_x_2)); float r2_y_3 = (r0_y_10 + r0_x_7); - float r2_y_4 = mad(sampleDiffuse_xyzw_5.z, _diff_Color.z, r2_y_3); + float r2_y_4 = mad(sampleDiffuse_r4_xyzw_5.z, _diff_Color.z, r2_y_3); float4 r2_xyzw_6 = (float4(r2_x_5, r2_y_4, r2_x_5, r2_x_5) * float4(0.33333334, 0.33333334, 0, 0)); float r2_x_6 = r2_xyzw_6.x; float r2_y_5 = r2_xyzw_6.y; - float4 monochromSelect_xyzw_8 = (r1_w_3.xxxx ? r2_y_5.xxxx : float4(r0_x_7, r0_y_10, r0_z_6, r0_x_7)); - float monochromSelect_x_8 = monochromSelect_xyzw_8.x; - float monochromSelect_y_11 = monochromSelect_xyzw_8.y; - float monochromSelect_z_7 = monochromSelect_xyzw_8.z; - float4 monochromSelect_xyzw_3 = (r1_w_3.xxxx ? r2_x_6.xxxx : float4(r1_x_2, r1_y_7, r1_z_7, r1_x_2)); - float monochromSelect_x_3 = monochromSelect_xyzw_3.x; - float monochromSelect_y_8 = monochromSelect_xyzw_3.y; - float monochromSelect_z_8 = monochromSelect_xyzw_3.z; - float4 r1_xyzw_4 = (-float4(monochromSelect_x_8, monochromSelect_y_11, monochromSelect_z_7, monochromSelect_x_8) + float4(monochromSelect_x_3, monochromSelect_y_8, monochromSelect_z_8, monochromSelect_x_3)); + float4 monochromSelect_r0_xyzw_8 = (r1_w_3.xxxx ? r2_y_5.xxxx : float4(r0_x_7, r0_y_10, r0_z_6, r0_x_7)); + float monochromSelect_r0_x_8 = monochromSelect_r0_xyzw_8.x; + float monochromSelect_r0_y_11 = monochromSelect_r0_xyzw_8.y; + float monochromSelect_r0_z_7 = monochromSelect_r0_xyzw_8.z; + float4 monochromSelect_r1_xyzw_3 = (r1_w_3.xxxx ? r2_x_6.xxxx : float4(r1_x_2, r1_y_7, r1_z_7, r1_x_2)); + float monochromSelect_r1_x_3 = monochromSelect_r1_xyzw_3.x; + float monochromSelect_r1_y_8 = monochromSelect_r1_xyzw_3.y; + float monochromSelect_r1_z_8 = monochromSelect_r1_xyzw_3.z; + float4 r1_xyzw_4 = (-float4(monochromSelect_r0_x_8, monochromSelect_r0_y_11, monochromSelect_r0_z_7, monochromSelect_r0_x_8) + float4(monochromSelect_r1_x_3, monochromSelect_r1_y_8, monochromSelect_r1_z_8, monochromSelect_r1_x_3)); float r1_x_4 = r1_xyzw_4.x; float r1_y_9 = r1_xyzw_4.y; float r1_z_9 = r1_xyzw_4.z; float r1_w_4 = (-r0_w_6 + _Color.w); - o.sv_Target0.xyzw = mad(i.texcoord4.xxxx, float4(r1_x_4, r1_y_9, r1_z_9, r1_w_4), float4(monochromSelect_x_8, monochromSelect_y_11, monochromSelect_z_7, r0_w_6)); + o.sv_Target0.xyzw = mad(i.texcoord4.xxxx, float4(r1_x_4, r1_y_9, r1_z_9, r1_w_4), float4(monochromSelect_r0_x_8, monochromSelect_r0_y_11, monochromSelect_r0_z_7, r0_w_6)); return o; } ENDHLSL diff --git a/docs/pr-temporary-register-names.md b/docs/pr-temporary-register-names.md new file mode 100644 index 0000000..da2e6a2 --- /dev/null +++ b/docs/pr-temporary-register-names.md @@ -0,0 +1,7 @@ +# Keep temporary register identities distinct + +Recovered semantic names can be shared by different registers. Include the register identity in temporary names so one assignment cannot overwrite an unrelated value. + +Validation: 52 C# tests and all four existing PicaVoxel extraction/decompilation tests pass. C# tests use an en-US default culture; the upstream fr-FR golden-output failure is addressed independently by the numeric-formatting PR. The golden output was regenerated by the emitter for this change only; existing comparison assertions are unchanged. + +This branch is based directly on upstream `1850608`; it does not include the other review branches. No proprietary fixtures or machine-specific paths are added. diff --git a/tests/UnityShaderDecompiler.Tests/TemporaryRegisterNamesTests.cs b/tests/UnityShaderDecompiler.Tests/TemporaryRegisterNamesTests.cs new file mode 100644 index 0000000..cda3592 --- /dev/null +++ b/tests/UnityShaderDecompiler.Tests/TemporaryRegisterNamesTests.cs @@ -0,0 +1,73 @@ +using System; +using System.Linq; +using Parser.Core.Hlsl; +using Parser.Core.Hlsl.Ast; +using Parser.Core.Analysis; +using Parser.Core.Optimizations; +using Parser.DXBC.Instructions; +using Parser.DXBC.IR; +using Xunit; + +namespace UnityShaderDecompiler.Tests; + +public sealed class TemporaryRegisterNamesTests +{ + [Fact] + public void SemanticNamesDoNotMergeDifferentTemporaryRegisters() + { + IRRegister first = Temp(1); + IRRegister second = Temp(4); + first.SymbolicName = "InputValue"; + second.SymbolicName = "InputValue"; + HlslFunctionNode function = new HlslFunctionNode { Name = "frag", Stage = HlslShaderStage.Fragment }; + function.Statements.Statements.Add(new HlslAssignmentStatement + { + Destination = first, Expression = Float(2), + }); + function.Statements.Statements.Add(new HlslAssignmentStatement + { + Destination = second, Expression = Float(3), + }); + function.Statements.Statements.Add(new HlslAssignmentStatement + { + Destination = Temp(5), + Expression = new IRExpression.RegisterExpression { Register = first }, + }); + + string source = Print(function); + Assert.Contains("float InputValue_r1_x_1 = 2", source); + Assert.Contains("float InputValue_r4_x_1 = 3", source); + Assert.Contains("= InputValue_r1_x_1;", source); + } + + + private static string Print(HlslFunctionNode function) + { + HlslShaderNode shader = new HlslShaderNode { Name = "Test/Scope" }; + HlslSubShaderNode subShader = new HlslSubShaderNode(); + subShader.Passes.Add(new HlslPassNode { FragmentFunction = function }); + shader.SubShaders.Add(subShader); + return HlslPrettyPrinter.Print(shader, fuseTemps: false); + } + + private static IRRegister Temp(uint index, byte mask = 1) + { + IRRegister register = new IRRegister + { + RegisterType = RegisterType.Temp, + Index = index, + Type = IRValueType.Float, + Mask = mask, + ComponentMode = Operand.OperandComponentMode.Mask, + }; + for (int component = 0; component < 4; component++) + if ((mask & (1 << component)) != 0) + register.SsaVersion[component] = 1; + return register; + } + + private static IRExpression.ConstantExpression Float(float value) => new() + { + RawValues = new[] { unchecked((uint)BitConverter.SingleToInt32Bits(value)) }, + }; +} From 54d66162cbe1a995da23d4c3917091943c94d6d6 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 12:25:26 +0100 Subject: [PATCH 13/93] Pair shader variants using serialized program references --- Shader Decompiler/Core/Hlsl/HlslAST.cs | 1 + Shader Decompiler/Core/Hlsl/HlslAstBuilder.cs | 1 + .../DXBC/Metadata/Shadermetadata.cs | 11 +++++ Shader Decompiler/Decompiler.cs | 43 +++++++++++++++++++ Shader Decompiler/VariantPairing.cs | 29 +++++++++++++ docs/pr-serialized-variant-pairing.md | 7 +++ libs/metadata.py | 40 +++++++++++------ .../VariantPairingTests.cs | 43 +++++++++++++++++++ tests/test_variant_metadata.py | 27 ++++++++++++ 9 files changed, 190 insertions(+), 12 deletions(-) create mode 100644 Shader Decompiler/VariantPairing.cs create mode 100644 docs/pr-serialized-variant-pairing.md create mode 100644 tests/UnityShaderDecompiler.Tests/VariantPairingTests.cs create mode 100644 tests/test_variant_metadata.py diff --git a/Shader Decompiler/Core/Hlsl/HlslAST.cs b/Shader Decompiler/Core/Hlsl/HlslAST.cs index 93478c0..2a4be75 100644 --- a/Shader Decompiler/Core/Hlsl/HlslAST.cs +++ b/Shader Decompiler/Core/Hlsl/HlslAST.cs @@ -176,6 +176,7 @@ public sealed class HlslRenderState public sealed class HlslPassNode { + public List DirectXPrograms { get; } = new(); public string Name { get; init; } = ""; public Dictionary Tags { get; } = new(); diff --git a/Shader Decompiler/Core/Hlsl/HlslAstBuilder.cs b/Shader Decompiler/Core/Hlsl/HlslAstBuilder.cs index 1472174..50bb615 100644 --- a/Shader Decompiler/Core/Hlsl/HlslAstBuilder.cs +++ b/Shader Decompiler/Core/Hlsl/HlslAstBuilder.cs @@ -89,6 +89,7 @@ public static HlslShaderNode BuildShell(ShaderMetadata metadata) }; foreach (var (k, v) in pass.Tags) passNode.Tags[k] = v; + passNode.DirectXPrograms.AddRange(pass.DirectXPrograms); foreach (CbufferMetadata cb in pass.ConstantBuffers) if (cb.Slot >= 0 && !passNode.Cbuffers.ContainsKey((cb.Slot, cb.Stage))) diff --git a/Shader Decompiler/DXBC/Metadata/Shadermetadata.cs b/Shader Decompiler/DXBC/Metadata/Shadermetadata.cs index 63bc48b..2e1c0c3 100644 --- a/Shader Decompiler/DXBC/Metadata/Shadermetadata.cs +++ b/Shader Decompiler/DXBC/Metadata/Shadermetadata.cs @@ -164,6 +164,7 @@ public static SubShaderMetadata From(JsonElement el) public sealed class PassMetadata { + public List DirectXPrograms { get; } = new(); public string Name { get; init; } = ""; public Dictionary Tags { get; init; } = new(); public JsonElement RenderState { get; init; } @@ -189,10 +190,20 @@ public static PassMetadata From(JsonElement el) foreach (var cb in cbs.EnumerateArray()) result.ConstantBuffers.Add(CbufferMetadata.From(cb)); + if (el.TryGetProperty("directXPrograms", out JsonElement programs)) + foreach (JsonElement program in programs.EnumerateArray()) + result.DirectXPrograms.Add(new ProgramVariantMetadata( + program.GetProperty("blobIndex").GetInt32(), + program.GetProperty("stage").GetString()!, + program.GetProperty("keywords").EnumerateArray().Select(k => k.GetString()!).ToArray(), + program.GetProperty("keywordMask").EnumerateArray().Select(k => k.GetString()!).ToArray())); + return result; } } +public sealed record ProgramVariantMetadata(int BlobIndex, string Stage, string[] Keywords, string[] KeywordMask); + /// One cbuffer in a pass: register slot -> name + variables. public sealed class CbufferMetadata { diff --git a/Shader Decompiler/Decompiler.cs b/Shader Decompiler/Decompiler.cs index 5a44dad..ee453e7 100644 --- a/Shader Decompiler/Decompiler.cs +++ b/Shader Decompiler/Decompiler.cs @@ -478,6 +478,12 @@ private static void BuildVariantPasses( if (prop.Kind == HlslPropertyKind.Texture) textureNames[texSlot++] = prop.Name; + if (allPasses.Any(p => p.DirectXPrograms.Count != 0)) + { + BuildSerializedVariantPasses(astShader, allPasses, candidates, textureNames, log); + return; + } + foreach (DecompiledSubprogram c in candidates) c.Hash = Convert.ToHexString(SHA256.HashData(c.DxbcBytes)); @@ -589,6 +595,43 @@ private static void BuildVariantPasses( log.WriteLine($"BuildVariantPasses: {groups.Count} pass group(s), {totalPasses} pass(es) emitted total."); } + private static void BuildSerializedVariantPasses( + HlslShaderNode shader, List shells, List candidates, + Dictionary textureNames, TextWriter log) + { + Dictionary byIndex = candidates.ToDictionary(c => c.Index); + HashSet covered = new HashSet(); + foreach (HlslPassNode shell in shells) + { + if (shell.DirectXPrograms.Count == 0) continue; + List<(int Vertex, int Fragment)> pairs = VariantPairing.Pair(shell.DirectXPrograms); + HlslSubShaderNode parent = shader.SubShaders.Single(ss => ss.Passes.Contains(shell)); + int insertAt = parent.Passes.IndexOf(shell); + foreach ((int vertexIndex, int fragmentIndex) in pairs) + { + if (!byIndex.TryGetValue(vertexIndex, out DecompiledSubprogram? vertex) || + !byIndex.TryGetValue(fragmentIndex, out DecompiledSubprogram? fragment)) + throw new InvalidDataException("Serialized variant references a missing DXBC program."); + if (vertex.Stage != HlslShaderStage.Vertex || fragment.Stage != HlslShaderStage.Fragment) + throw new InvalidDataException("Serialized variant stage does not match its DXBC program."); + string[] inputs = fragment.InputInterpolators.Split(';', StringSplitOptions.RemoveEmptyEntries); + string[] outputs = vertex.OutputInterpolators.Split(';', StringSplitOptions.RemoveEmptyEntries); + if (!inputs.All(outputs.Contains)) + throw new InvalidDataException($"Variant {vertexIndex}/{fragmentIndex} has incompatible interpolators."); + HlslPassNode pass = insertAt == parent.Passes.IndexOf(shell) ? shell : ClonePass(shell); + if (!ReferenceEquals(pass, shell)) parent.Passes.Insert(insertAt, pass); + AttachPassProgram(pass, vertex, textureNames, log); + AttachPassProgram(pass, fragment, textureNames, log); + covered.Add(vertexIndex); + covered.Add(fragmentIndex); + insertAt++; + } + log.WriteLine($"Serialized variants: {pairs.Count} stage pairs."); + } + if (byIndex.Keys.Except(covered).Any()) + throw new InvalidDataException("DXBC programs are missing from serialized variant coverage."); + } + // Attaches one candidate's function, resources and structs to a pass, // unioning cbuffer members the way the previous streaming loop did. private static void AttachPassProgram( diff --git a/Shader Decompiler/VariantPairing.cs b/Shader Decompiler/VariantPairing.cs new file mode 100644 index 0000000..c0004e4 --- /dev/null +++ b/Shader Decompiler/VariantPairing.cs @@ -0,0 +1,29 @@ +using Parser.DXBC.Metadata; + +namespace Parser; + +internal static class VariantPairing +{ + internal static List<(int Vertex, int Fragment)> Pair(IReadOnlyList programs) + { + if (programs.Any(p => p.Stage is not ("Vertex" or "Fragment"))) + throw new InvalidDataException("Serialized variant pairing currently supports vertex/fragment stages only."); + ProgramVariantMetadata[] vertices = programs.Where(p => p.Stage == "Vertex").ToArray(); + ProgramVariantMetadata[] fragments = programs.Where(p => p.Stage == "Fragment").ToArray(); + if (programs.Any(p => p.Keywords.Except(p.KeywordMask).Any())) + throw new InvalidDataException("A shader variant enables a keyword outside its stage mask."); + List<(int Vertex, int Fragment)> pairs = new List<(int Vertex, int Fragment)>(); + foreach (ProgramVariantMetadata vertex in vertices) + foreach (ProgramVariantMetadata fragment in fragments) + { + HashSet shared = vertex.KeywordMask.Intersect(fragment.KeywordMask).ToHashSet(); + if (vertex.Keywords.Where(shared.Contains).ToHashSet().SetEquals(fragment.Keywords.Where(shared.Contains))) + pairs.Add((vertex.BlobIndex, fragment.BlobIndex)); + } + // A fragment-only keyword can reuse a vertex program; pairing is not 1:1. + if (vertices.Any(v => !pairs.Any(p => p.Vertex == v.BlobIndex)) || + fragments.Any(f => !pairs.Any(p => p.Fragment == f.BlobIndex)) || pairs.Count == 0) + throw new InvalidDataException("Serialized shader variants contain an unmatched stage."); + return pairs.Distinct().ToList(); + } +} diff --git a/docs/pr-serialized-variant-pairing.md b/docs/pr-serialized-variant-pairing.md new file mode 100644 index 0000000..a1e9a76 --- /dev/null +++ b/docs/pr-serialized-variant-pairing.md @@ -0,0 +1,7 @@ +# Pair shader variants using serialized program references + +Use serialized indices and stage keyword masks to pair vertex and fragment programs, including vertex reuse for fragment-only keywords. Reject missing programs and incompatible stage interfaces. This preserves the existing expansion into separate passes; it does not implement runtime keyword selection. + +Validation: 54 C# tests and all four existing PicaVoxel extraction/decompilation tests pass. C# tests use an en-US default culture; the upstream fr-FR golden-output failure is addressed independently by the numeric-formatting PR. 1 Python tests also pass. + +This branch is based directly on upstream `1850608`; it does not include the other review branches. No proprietary fixtures or machine-specific paths are added. diff --git a/libs/metadata.py b/libs/metadata.py index 0861c10..7f112a0 100644 --- a/libs/metadata.py +++ b/libs/metadata.py @@ -1,10 +1,10 @@ """ -Stage 0.2 — Metadata Extraction +Stage 0.2 — Metadata Extraction Pulls everything the backend (C#) needs out of the raw Unity shader JSON (the AssetStudio export) into a single metadata.json, keyed by shader name. -This does NOT touch m_CompressedBlob — that stays in blob.py/ShaderBlob. +This does NOT touch m_CompressedBlob — that stays in blob.py/ShaderBlob. """ import json @@ -81,7 +81,7 @@ def _sub_shaders(self, parsed): subshaders.append({ "tags": self._tags(ss.get("m_Tags", {})), "lod": ss.get("m_LOD"), - "passes": [self._pass(p, name_layouts) for p in raw_passes], + "passes": [self._pass(p, name_layouts, parsed.get("m_KeywordNames", [])) for p in raw_passes], }) return subshaders @@ -127,7 +127,7 @@ def _name_layouts(raw_passes): variables.append(entry) return layouts - def _pass(self, p, name_layouts=None): + def _pass(self, p, name_layouts=None, keyword_names=()): state = p.get("m_State", {}) prog_keys = ["m_ProgVertex", "m_ProgFragment", "m_ProgGeometry", "m_ProgHull", "m_ProgDomain", "m_ProgRayTracing"] @@ -154,19 +154,38 @@ def _pass(self, p, name_layouts=None): "tags": self._tags(state.get("m_Tags", {})), "renderState": self._render_state(state), "programs": programs, + "directXPrograms": self._directx_programs(p, keyword_names), "constantBuffers": self._constant_buffers(p, prog_keys, index_to_name, name_layouts), "textures": p.get("m_SerializedProgram", {}).get("m_TextureParameters", []), "samplers": p.get("m_SerializedProgram", {}).get("m_SamplerParameters", []), "keywords": p.get("m_NameIndices", []), } + @staticmethod + def _directx_programs(shader_pass, keyword_names): + result = [] + for stage in ("Vertex", "Fragment", "Geometry", "Hull", "Domain"): + program = shader_pass.get("m_Prog" + stage, {}) + for group in program.get("m_PlayerSubPrograms", []): + for entry in group: + if not 15 <= entry["m_GpuProgramType"] <= 22: + continue + indices = entry["m_KeywordIndices"] + mask = program["m_SerializedKeywordStateMask"] + if any(not 0 <= index < len(keyword_names) for index in indices + mask): + raise ValueError("Shader variant references an invalid keyword index") + result.append({"stage": stage, "blobIndex": entry["m_BlobIndex"], + "keywords": sorted(keyword_names[index] for index in indices), + "keywordMask": sorted(keyword_names[index] for index in mask)}) + return result + def _constant_buffers(self, p, prog_keys, index_to_name, name_layouts=None): # Unity serializes the constant-buffer layout (name + per-variable - # byte offset) in each stage's m_CommonParameters — this is the + # byte offset) in each stage's m_CommonParameters — this is the # per-pass replacement for the RDEF chunk Unity strips from shipped # bytecode. Each stage binds its OWN table (cb0 in the vertex # program can be a different buffer than cb0 in the fragment - # program), so cbuffers are keyed by (stage, slot) — never merged + # program), so cbuffers are keyed by (stage, slot) — never merged # across stages, or different buffers that share a slot number # would collide at the same byte offsets. by_key = {} @@ -205,7 +224,6 @@ def _next_slot(): "slot": slot, "stage": stage, "name": index_to_name.get(b.get("m_NameIndex"), ""), - "size": b.get("m_Size", 0), "variables": [], }) for v in b.get("m_VectorParams", []) or []: @@ -226,7 +244,7 @@ def _next_slot(): # A program can bind a buffer (its name index appears in # m_ConstantBufferBindings) without listing it in - # m_ConstantBuffers — Unity only serializes the buffers the + # m_ConstantBuffers — Unity only serializes the buffers the # stage touches. When the bound name's layout is known from # another pass/stage of the same shader, reuse it so reads of # that (stage, slot) still resolve to real names. @@ -244,7 +262,6 @@ def _next_slot(): "slot": slot, "stage": stage, "name": cb_name, - "size": 0, "variables": [dict(v) for v in name_layouts[cb_name]], } @@ -262,7 +279,6 @@ def _next_slot(): "slot": cb["slot"], "stage": cb["stage"], "name": cb["name"], - "size": cb["size"], "variables": variables, }) return out @@ -270,7 +286,7 @@ def _next_slot(): @staticmethod def _unwrap(v, default=None): # Unity wraps most scalar render state in - # {"m_Name": "...", "m_Value": N} — flatten to just N. + # {"m_Name": "...", "m_Value": N} — flatten to just N. if isinstance(v, dict) and "m_Value" in v: return v["m_Value"] return v if v is not None else default @@ -280,7 +296,7 @@ def _render_state(self, state): # like m_Culling/m_ZTest/m_ZWrite/m_RtBlend0 (values wrapped as # {"m_Value": N}). That's the schema the C# side (HlslRenderstateBuilder) # consumes. A few exports use a nested rasterState/depthState shape - # instead — fall back to that when the flat keys are absent. + # instead — fall back to that when the flat keys are absent. if not isinstance(state, dict): return {} diff --git a/tests/UnityShaderDecompiler.Tests/VariantPairingTests.cs b/tests/UnityShaderDecompiler.Tests/VariantPairingTests.cs new file mode 100644 index 0000000..110ade0 --- /dev/null +++ b/tests/UnityShaderDecompiler.Tests/VariantPairingTests.cs @@ -0,0 +1,43 @@ +using Parser; +using Parser.DXBC.Metadata; + +namespace UnityShaderDecompiler.Tests; + +public class VariantPairingTests +{ + [Fact] + public void FragmentOnlyKeywordReusesTheVertexProgram() + { + ProgramVariantMetadata[] programs = new[] + { + new ProgramVariantMetadata(1, "Vertex", [], ["SHARED"]), + new ProgramVariantMetadata(2, "Fragment", [], ["SHARED", "DETAIL"]), + new ProgramVariantMetadata(3, "Fragment", ["DETAIL"], ["SHARED", "DETAIL"]), + }; + Assert.Equal(new[] { (1, 2), (1, 3) }, VariantPairing.Pair(programs)); + } + + [Fact] + public void SharedKeywordMatchesByStateInsteadOfSerializedOrder() + { + ProgramVariantMetadata[] programs = new[] + { + new ProgramVariantMetadata(1, "Vertex", [], ["MODE"]), + new ProgramVariantMetadata(2, "Vertex", ["MODE"], ["MODE"]), + new ProgramVariantMetadata(3, "Fragment", ["MODE"], ["MODE"]), + new ProgramVariantMetadata(4, "Fragment", [], ["MODE"]), + }; + Assert.Equal(new[] { (1, 4), (2, 3) }, VariantPairing.Pair(programs)); + } + + [Fact] + public void MissingVariantFailsEvenWhenItsKeywordIsNeverEnabledInTheOtherStage() + { + ProgramVariantMetadata[] programs = new[] + { + new ProgramVariantMetadata(1, "Vertex", [], ["MODE"]), + new ProgramVariantMetadata(2, "Fragment", ["MODE"], ["MODE"]), + }; + Assert.Throws(() => VariantPairing.Pair(programs)); + } +} diff --git a/tests/test_variant_metadata.py b/tests/test_variant_metadata.py new file mode 100644 index 0000000..7c515dc --- /dev/null +++ b/tests/test_variant_metadata.py @@ -0,0 +1,27 @@ +import json +import tempfile +import unittest +from pathlib import Path + +from libs.metadata import ShaderMetadata + + +class ShaderMetadataTests(unittest.TestCase): + def test_variant_references_keep_stage_keyword_masks(self): + shader_pass = {"m_ProgFragment": { + "m_SerializedKeywordStateMask": [0, 1], + "m_PlayerSubPrograms": [[ + {"m_GpuProgramType": 17, "m_BlobIndex": 7, "m_KeywordIndices": [1]}, + {"m_GpuProgramType": 23, "m_BlobIndex": 8, "m_KeywordIndices": []}, + ]], + }} + self.assertEqual(ShaderMetadata._directx_programs(shader_pass, ["MODE", "DETAIL"]), [ + {"stage": "Fragment", "blobIndex": 7, "keywords": ["DETAIL"], + "keywordMask": ["DETAIL", "MODE"]}, + ]) + with self.assertRaises(ValueError): + ShaderMetadata._directx_programs(shader_pass, ["MODE"]) + + +if __name__ == "__main__": + unittest.main() From de1246b2efc77907b5f8d905e195b2cb5025a64c Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 12:25:31 +0100 Subject: [PATCH 14/93] Use the predicate assertion for sampler binding selection --- tests/UnityShaderDecompiler.Tests/SamplerBindingTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/UnityShaderDecompiler.Tests/SamplerBindingTests.cs b/tests/UnityShaderDecompiler.Tests/SamplerBindingTests.cs index 9831678..ed40996 100644 --- a/tests/UnityShaderDecompiler.Tests/SamplerBindingTests.cs +++ b/tests/UnityShaderDecompiler.Tests/SamplerBindingTests.cs @@ -10,8 +10,8 @@ public class SamplerBindingTests public void SameRecoveredNameDoesNotMergeDifferentSamplerSlots() { DecompilationResult result = FixtureCache.Decompile("HOLO_Holo"); - HlslPassNode pass = Assert.Single(result.Passes.Where(p => - p.Resources.Any(r => r.Kind == HlslResourceKind.Sampler && r.Slot == 2))); + HlslPassNode pass = Assert.Single(result.Passes, p => + p.Resources.Any(r => r.Kind == HlslResourceKind.Sampler && r.Slot == 2)); HlslResourceNode first = Assert.Single(pass.Resources, r => r.Kind == HlslResourceKind.Sampler && r.Slot == 0); HlslResourceNode second = Assert.Single(pass.Resources, From 6380b0246aed2ca9b7f7e92ea3bc082c37b1d77c Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 12:26:31 +0100 Subject: [PATCH 15/93] Preserve resource bindings and raw constant-buffer layout --- docs/pr-resource-binding-layouts.md | 2 +- .../GoldenNativeCompilationTests.cs | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 tests/UnityShaderDecompiler.Tests/GoldenNativeCompilationTests.cs diff --git a/docs/pr-resource-binding-layouts.md b/docs/pr-resource-binding-layouts.md index 1db0a51..fc0bc48 100644 --- a/docs/pr-resource-binding-layouts.md +++ b/docs/pr-resource-binding-layouts.md @@ -2,6 +2,6 @@ Keep resources with different slots distinct even when their recovered names coincide. When a buffer needs raw fallback storage, emit one array starting at byte zero while retaining its name, binding and declared size. Native reflection tests verify the physical buffer layout. -Validation: 55 C# tests and all four existing PicaVoxel extraction/decompilation tests pass. C# tests use an en-US default culture; the upstream fr-FR golden-output failure is addressed independently by the numeric-formatting PR. The golden output was regenerated by the emitter for this change only; existing comparison assertions are unchanged. +Validation: 57 C# tests and all four existing PicaVoxel extraction/decompilation tests pass. C# tests use an en-US default culture; the upstream fr-FR golden-output failure is addressed independently by the numeric-formatting PR. This branch is based directly on upstream `1850608`; it does not include the other review branches. No proprietary fixtures or machine-specific paths are added. diff --git a/tests/UnityShaderDecompiler.Tests/GoldenNativeCompilationTests.cs b/tests/UnityShaderDecompiler.Tests/GoldenNativeCompilationTests.cs new file mode 100644 index 0000000..f68bb88 --- /dev/null +++ b/tests/UnityShaderDecompiler.Tests/GoldenNativeCompilationTests.cs @@ -0,0 +1,24 @@ +using System; +using System.Text.RegularExpressions; +using Parser; + +namespace UnityShaderDecompiler.Tests; + +public class GoldenNativeCompilationTests +{ + [Theory] + [InlineData("vert", "vs_5_0")] + [InlineData("frag", "ps_5_0")] + [Trait("Category", "D3D")] + public void GoldenFixtureProducesCompilableStages(string entry, string profile) + { + if (!OperatingSystem.IsWindows()) return; + + DecompilationResult result = FixtureCache.Decompile("HOLO_Holo"); + MatchCollection blocks = Regex.Matches(result.ShaderText, + @"HLSLPROGRAM(.*?)ENDHLSL", RegexOptions.Singleline); + Assert.NotEmpty(blocks); + foreach (Match block in blocks) + Assert.NotEmpty(RecompileVerify.CompileHlsl(block.Groups[1].Value, entry, profile)); + } +} From 257e85a3ca9fc9b27855168508f198e176b1c75e Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 12:32:03 +0100 Subject: [PATCH 16/93] Document combined validation with real exports from both tools --- docs/pr-metadata-offsets.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/pr-metadata-offsets.md b/docs/pr-metadata-offsets.md index d1c1854..7d67e03 100644 --- a/docs/pr-metadata-offsets.md +++ b/docs/pr-metadata-offsets.md @@ -6,4 +6,6 @@ Read `m_OffsetInConstantBuffer` when present and legacy `m_Index` otherwise. Ass Validation: ten generic Python tests and all four existing PicaVoxel extraction/decompilation tests pass. Two additional local shaders were exported directly from the same game bundle with AssetStudioMod CLI v0.20.0 and AssetRipper: decompressed blobs and extracted metadata match across exporters. Those external fixtures are not included in this PR. +With the separate emitter fixes combined, both shaders produce identical ShaderLab/HLSL/DXBC from either exporter. All four outputs pass native recompilation and deterministic DXBC execution comparisons, including serialized constant inputs. These finite interpreter checks are not GPU-rendering equivalence proofs. + All 51 baseline C# tests pass with an en-US default culture. The unchanged golden test fails under fr-FR on upstream because of decimal separators; the numeric-formatting PR fixes that independently. No existing assertions or golden files are changed here. From a9d8b85da1767c1340217c81fc21700b78e42e9a Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 12:58:14 +0100 Subject: [PATCH 17/93] Verify integer move bits through native compiler round trips --- docs/pr-raw-register-semantics.md | 6 +- .../RawRegisterSemanticsTests.cs | 32 ++++++ .../RegisterRoundTrip.cs | 104 ++++++++++++++++++ 3 files changed, 139 insertions(+), 3 deletions(-) create mode 100644 tests/UnityShaderDecompiler.Tests/RegisterRoundTrip.cs diff --git a/docs/pr-raw-register-semantics.md b/docs/pr-raw-register-semantics.md index ac3160d..1f800a7 100644 --- a/docs/pr-raw-register-semantics.md +++ b/docs/pr-raw-register-semantics.md @@ -1,7 +1,7 @@ # Preserve register bits and arithmetic in no-fuse mode -Keep temporary registers as raw uint bits with explicit typed reads, full-bit comparison masks and integer source modifiers applied after interpretation. Disable arithmetic pattern rewrites in no-fuse mode and preserve the addition before fractional-part evaluation under native recompilation. +With `--no-fuse-temps`, interpreting temporary registers as numeric floats can corrupt integer moves and comparison masks. Preserve raw uint storage with typed reads and outputs; for example, `0x01000001` must remain that exact integer instead of rounding to `0x01000000`. Integer negation applies after bit interpretation, and arithmetic rewrites must not remove the addition before fractional-part evaluation. -Validation: 54 C# tests and all four existing PicaVoxel extraction/decompilation tests pass. C# tests use an en-US default culture; the upstream fr-FR golden-output failure is addressed independently by the numeric-formatting PR. +Native round-trip tests cover `mov` and `movc` to signed and unsigned outputs, both conditional outcomes, and bit patterns representing NaNs, subnormals and signed zero. A restricted bytecode evaluator compares exact output words against explicit expectations and rejects unexpected operations. Disabling bit preservation makes all four new cases fail. These are compiler/bytecode checks, not GPU execution tests. -This branch is based directly on upstream `1850608`; it does not include the other review branches. No proprietary fixtures or machine-specific paths are added. +Validation: 58 C# tests and four PicaVoxel end-to-end cases pass on Windows. The isolated suite uses en-US because upstream's fr-FR golden-output failure is fixed separately in #5. Existing upstream assertions are unchanged. diff --git a/tests/UnityShaderDecompiler.Tests/RawRegisterSemanticsTests.cs b/tests/UnityShaderDecompiler.Tests/RawRegisterSemanticsTests.cs index 1788b4d..02658b9 100644 --- a/tests/UnityShaderDecompiler.Tests/RawRegisterSemanticsTests.cs +++ b/tests/UnityShaderDecompiler.Tests/RawRegisterSemanticsTests.cs @@ -12,6 +12,38 @@ namespace UnityShaderDecompiler.Tests; public sealed class RawRegisterSemanticsTests { + [Theory] + [InlineData("uint", false)] + [InlineData("int", false)] + [InlineData("uint", true)] + [InlineData("int", true)] + [Trait("Category", "D3D")] + public void IntegerMovesPreserveBitsThroughNativeRoundTrip(string type, bool conditional) + { + if (!OperatingSystem.IsWindows()) return; + + // These integers also encode NaNs, a subnormal and negative zero as floats. + uint[] selected = { 0xffffffffu, 0x7fc12345u, 0x00000001u, 0x80000000u }; + uint[] alternate = { 0x01000001u, 0xffc54321u, 0x7fffffffu, 0x80000001u }; + string first = $"{type}4(0xffffffffu, 0x7fc12345u, 1u, 0x80000000u)"; + string second = $"{type}4(0x01000001u, 0xffc54321u, 0x7fffffffu, 0x80000001u)"; + string value = conditional ? $"condition ? {first} : {second}" : first; + Parser.DXBC.DxbcFile original = RegisterRoundTrip.Compile( + $"{type}4 main(nointerpolation uint condition : TEXCOORD0) : SV_Target {{ return {value}; }}"); + Assert.Contains(original.Shader!.Instructions, instruction => + instruction.Opcode == (conditional ? Opcode.MovC : Opcode.Mov)); + Parser.DXBC.DxbcFile rebuilt = RegisterRoundTrip.Recompile(original); + Assert.Equal(original.OutputSignature!.Elements[0].ComponentType, + rebuilt.OutputSignature!.Elements[0].ComponentType); + foreach (uint condition in new uint[] { 0, 1, 0x80000000u, uint.MaxValue }) + { + uint[] input = { condition, 0, 0, 0 }; + uint[] expected = conditional && condition == 0 ? alternate : selected; + Assert.Equal(expected, RegisterRoundTrip.Evaluate(original, input)); + Assert.Equal(expected, RegisterRoundTrip.Evaluate(rebuilt, input)); + } + } + [Fact] public void IntegerSourceModifierIsAppliedAfterBitInterpretation() { diff --git a/tests/UnityShaderDecompiler.Tests/RegisterRoundTrip.cs b/tests/UnityShaderDecompiler.Tests/RegisterRoundTrip.cs new file mode 100644 index 0000000..26cc965 --- /dev/null +++ b/tests/UnityShaderDecompiler.Tests/RegisterRoundTrip.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using AssetStudio; +using Parser; +using Parser.Core.Hlsl; +using Parser.Core.Hlsl.Ast; +using Parser.DXBC; +using Parser.DXBC.Instructions; +using Parser.DXBC.IR; +using Xunit; + +namespace UnityShaderDecompiler.Tests; + +internal static class RegisterRoundTrip +{ + internal static DxbcFile Compile(string source, string entry = "main") + { + DxbcFile file = new DxbcFile(); + file.Load(RecompileVerify.CompileHlsl(source, entry, "ps_5_0")); + return file; + } + + internal static DxbcFile Recompile(DxbcFile original) + { + IRPipeline.Result pipeline = IRPipeline.Run(original, preserveArithmetic: true); + Assert.True(pipeline.SsaVerification.IsValid); + HlslFunctionNode function = HlslAstBuilder.BuildFunction("RoundTrip", + ShaderGpuProgramType.DX11PixelSM50, pipeline.Program.Declarations, + pipeline.Blocks, original.InputSignature, original.OutputSignature)!; + function.RawTemporaryStorage = true; + HlslPassNode pass = new HlslPassNode { FragmentFunction = function }; + pass.Structs.Add(function.InputStruct!); + pass.Structs.Add(function.OutputStruct!); + pass.Resources.AddRange(HlslAstBuilder.BuildResources(pipeline.Program.Declarations, + original.ResourceDefinition, pipeline.Blocks)); + HlslSubShaderNode subShader = new HlslSubShaderNode(); + subShader.Passes.Add(pass); + HlslShaderNode shader = new HlslShaderNode { Name = "Test/RegisterRoundTrip" }; + shader.SubShaders.Add(subShader); + string shaderText = HlslPrettyPrinter.Print(shader, fuseTemps: false); + string hlsl = Regex.Match(shaderText, @"HLSLPROGRAM(.*?)ENDHLSL", RegexOptions.Singleline).Groups[1].Value; + return Compile(hlsl, function.Name); + } + + // Deliberately limited to the bit-copy fixtures below; unexpected arithmetic fails. + internal static uint[] Evaluate(DxbcFile file, uint[] input) + { + Dictionary<(RegisterType, uint), uint[]> registers = new Dictionary<(RegisterType, uint), uint[]> + { + [(RegisterType.Input, 0)] = input, + }; + foreach (Instruction instruction in file.Shader!.Instructions) + { + if (instruction.Opcode.ToString().StartsWith("Dcl", StringComparison.Ordinal)) continue; + if (instruction.Opcode == Opcode.Ret) return registers[(RegisterType.Output, 0)]; + Assert.True(instruction.Opcode is Opcode.Mov or Opcode.MovC or Opcode.INe or Opcode.IEq + or Opcode.And, $"Unexpected opcode: {instruction.Opcode}"); + Operand destination = instruction.Operands[0]; + uint[] result = new uint[4]; + for (int lane = 0; lane < 4; lane++) + { + if ((destination.Mask & (1 << lane)) == 0) continue; + uint first = Read(instruction.Operands[1], lane, registers); + if (instruction.Opcode == Opcode.Mov) result[lane] = first; + else if (instruction.Opcode == Opcode.MovC) + result[lane] = Read(instruction.Operands[first != 0 ? 2 : 3], lane, registers); + else + { + uint second = Read(instruction.Operands[2], lane, registers); + result[lane] = instruction.Opcode switch + { + Opcode.INe => first != second ? uint.MaxValue : 0u, + Opcode.IEq => first == second ? uint.MaxValue : 0u, + Opcode.And => first & second, + _ => throw new InvalidOperationException(), + }; + } + } + (RegisterType, uint) key = (destination.RegisterType, destination.RegisterIndex); + if (!registers.TryGetValue(key, out uint[]? target)) registers[key] = target = new uint[4]; + for (int lane = 0; lane < 4; lane++) + if ((destination.Mask & (1 << lane)) != 0) target[lane] = result[lane]; + } + throw new InvalidOperationException("Fixture has no return instruction."); + } + + private static int Component(Operand operand, int lane) => operand.ComponentMode switch + { + Operand.OperandComponentMode.Select1 => operand.Component, + Operand.OperandComponentMode.Swizzle => (operand.Swizzle >> (2 * lane)) & 3, + _ => lane, + }; + + private static uint Read(Operand operand, int lane, Dictionary<(RegisterType, uint), uint[]> registers) + { + Assert.Equal(ShdrParser.OperandModifier.None, operand.Modifier); + int component = Component(operand, lane); + if (operand.RegisterType == RegisterType.Immediate32) + return operand.Immediate32Values![operand.Immediate32Values.Length == 1 ? 0 : component]; + return registers[(operand.RegisterType, operand.RegisterIndex)][component]; + } +} From 8bdfda0c2e05617f9a00f1a48c800b383c3ea2f6 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 12:58:54 +0100 Subject: [PATCH 18/93] Retain constant buffer sizes when extracting variant metadata --- docs/pr-serialized-variant-pairing.md | 6 ++--- libs/metadata.py | 17 ++++++++------ tests/test_variant_metadata.py | 32 +++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 10 deletions(-) diff --git a/docs/pr-serialized-variant-pairing.md b/docs/pr-serialized-variant-pairing.md index a1e9a76..d4c30b6 100644 --- a/docs/pr-serialized-variant-pairing.md +++ b/docs/pr-serialized-variant-pairing.md @@ -1,7 +1,7 @@ # Pair shader variants using serialized program references -Use serialized indices and stage keyword masks to pair vertex and fragment programs, including vertex reuse for fragment-only keywords. Reject missing programs and incompatible stage interfaces. This preserves the existing expansion into separate passes; it does not implement runtime keyword selection. +Grouping programs by blob order can pair unrelated variants. Use serialized program indices and pair stage variants when their enabled keywords agree on the intersection of their keyword masks. Stage-only keywords remain independent: a vertex variant can therefore pair with multiple fragment-only variants. Reject missing programs and incompatible interfaces. -Validation: 54 C# tests and all four existing PicaVoxel extraction/decompilation tests pass. C# tests use an en-US default culture; the upstream fr-FR golden-output failure is addressed independently by the numeric-formatting PR. 1 Python tests also pass. +Preserve constant-buffer sizes in the extracted metadata, including the existing zero sentinel for a binding with no serialized extent. A Python serialization test covers both paths. Runtime keyword selection remains unimplemented: compatible combinations are still emitted as separate passes. -This branch is based directly on upstream `1850608`; it does not include the other review branches. No proprietary fixtures or machine-specific paths are added. +Validation: 54 C# tests, two Python tests and all four PicaVoxel end-to-end cases pass. The isolated C# suite uses en-US; upstream's fr-FR golden-output failure is addressed separately in #5. Regenerate shared golden references after combining emitter changes. diff --git a/libs/metadata.py b/libs/metadata.py index 7f112a0..2a8f8d1 100644 --- a/libs/metadata.py +++ b/libs/metadata.py @@ -1,10 +1,10 @@ """ -Stage 0.2 — Metadata Extraction +Stage 0.2 — Metadata Extraction Pulls everything the backend (C#) needs out of the raw Unity shader JSON (the AssetStudio export) into a single metadata.json, keyed by shader name. -This does NOT touch m_CompressedBlob — that stays in blob.py/ShaderBlob. +This does NOT touch m_CompressedBlob — that stays in blob.py/ShaderBlob. """ import json @@ -181,11 +181,11 @@ def _directx_programs(shader_pass, keyword_names): def _constant_buffers(self, p, prog_keys, index_to_name, name_layouts=None): # Unity serializes the constant-buffer layout (name + per-variable - # byte offset) in each stage's m_CommonParameters — this is the + # byte offset) in each stage's m_CommonParameters — this is the # per-pass replacement for the RDEF chunk Unity strips from shipped # bytecode. Each stage binds its OWN table (cb0 in the vertex # program can be a different buffer than cb0 in the fragment - # program), so cbuffers are keyed by (stage, slot) — never merged + # program), so cbuffers are keyed by (stage, slot) — never merged # across stages, or different buffers that share a slot number # would collide at the same byte offsets. by_key = {} @@ -224,6 +224,7 @@ def _next_slot(): "slot": slot, "stage": stage, "name": index_to_name.get(b.get("m_NameIndex"), ""), + "size": b.get("m_Size", 0), "variables": [], }) for v in b.get("m_VectorParams", []) or []: @@ -244,7 +245,7 @@ def _next_slot(): # A program can bind a buffer (its name index appears in # m_ConstantBufferBindings) without listing it in - # m_ConstantBuffers — Unity only serializes the buffers the + # m_ConstantBuffers — Unity only serializes the buffers the # stage touches. When the bound name's layout is known from # another pass/stage of the same shader, reuse it so reads of # that (stage, slot) still resolve to real names. @@ -262,6 +263,7 @@ def _next_slot(): "slot": slot, "stage": stage, "name": cb_name, + "size": 0, "variables": [dict(v) for v in name_layouts[cb_name]], } @@ -279,6 +281,7 @@ def _next_slot(): "slot": cb["slot"], "stage": cb["stage"], "name": cb["name"], + "size": cb["size"], "variables": variables, }) return out @@ -286,7 +289,7 @@ def _next_slot(): @staticmethod def _unwrap(v, default=None): # Unity wraps most scalar render state in - # {"m_Name": "...", "m_Value": N} — flatten to just N. + # {"m_Name": "...", "m_Value": N} — flatten to just N. if isinstance(v, dict) and "m_Value" in v: return v["m_Value"] return v if v is not None else default @@ -296,7 +299,7 @@ def _render_state(self, state): # like m_Culling/m_ZTest/m_ZWrite/m_RtBlend0 (values wrapped as # {"m_Value": N}). That's the schema the C# side (HlslRenderstateBuilder) # consumes. A few exports use a nested rasterState/depthState shape - # instead — fall back to that when the flat keys are absent. + # instead — fall back to that when the flat keys are absent. if not isinstance(state, dict): return {} diff --git a/tests/test_variant_metadata.py b/tests/test_variant_metadata.py index 7c515dc..0d63ccc 100644 --- a/tests/test_variant_metadata.py +++ b/tests/test_variant_metadata.py @@ -7,6 +7,38 @@ class ShaderMetadataTests(unittest.TestCase): + def test_constant_buffer_sizes_survive_serialization(self): + shader_pass = { + "m_NameIndices": {"Parameters": 0}, + "m_ProgVertex": {"m_CommonParameters": { + "m_ConstantBuffers": [{"m_NameIndex": 0, "m_Size": 64}], + "m_ConstantBufferBindings": [{"m_NameIndex": 0, "m_Index": 2}], + }}, + "m_ProgFragment": {"m_CommonParameters": { + "m_ConstantBufferBindings": [{"m_NameIndex": 0, "m_Index": 3}], + }}, + } + source = {"m_Name": "Test/BufferLayout", "m_ParsedForm": { + "m_SubShaders": [{"m_Passes": [shader_pass]}], + }} + with tempfile.TemporaryDirectory() as directory: + source_path = Path(directory) / "shader.json" + output_path = Path(directory) / "metadata.json" + source_path.write_text(json.dumps(source), encoding="utf-8") + ShaderMetadata(source_path).save(output_path) + metadata = json.loads(output_path.read_text(encoding="utf-8")) + buffers = metadata["subShaders"][0]["passes"][0]["constantBuffers"] + by_stage = {buffer["stage"]: buffer for buffer in buffers} + self.assertEqual(by_stage["Vertex"], { + "name": "Parameters", "stage": "Vertex", "slot": 2, + "size": 64, "variables": [], + }) + # A binding without a serialized extent retains the existing zero sentinel. + self.assertEqual(by_stage["Fragment"], { + "name": "Parameters", "stage": "Fragment", "slot": 3, + "size": 0, "variables": [], + }) + def test_variant_references_keep_stage_keyword_masks(self): shader_pass = {"m_ProgFragment": { "m_SerializedKeywordStateMask": [0, 1], From 714a899cc6ab254efc9fe22ea8b321e2ec3ebafc Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 12:58:55 +0100 Subject: [PATCH 19/93] Repair metadata comment encoding and clarify reproducible validation --- docs/pr-metadata-offsets.md | 10 +++------- libs/metadata.py | 14 +++++++------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/docs/pr-metadata-offsets.md b/docs/pr-metadata-offsets.md index 7d67e03..a3117ea 100644 --- a/docs/pr-metadata-offsets.md +++ b/docs/pr-metadata-offsets.md @@ -1,11 +1,7 @@ # Accept AssetStudio and AssetRipper shader JSON schemas -Normalize the two exporters' field names, serialized maps, property defaults and render states at the JSON input boundary. Recover the ShaderLab name from ParsedForm when the object name is empty. Both metadata extraction and blob extraction use the same loader; the original JSON is never rewritten. +Extraction fails when an export uses legacy `m_Index` offsets or different field/map encodings. Normalize both exporters at the input boundary, prefer `m_OffsetInConstantBuffer` when present, and fall back to `m_Index`. Reject missing/invalid offsets, conflicting aliases and duplicate map keys. Source JSON files remain unchanged. -Read `m_OffsetInConstantBuffer` when present and legacy `m_Index` otherwise. AssetRipper's `Pass002_RenameSubnodes` explicitly normalizes this rename. Both layout readers reject invalid or missing offsets instead of inventing zero. Conflicting aliases and duplicate map keys also fail explicitly. +Reproducible validation: ten generic Python tests, 51 baseline C# tests and all four checked-in PicaVoxel extraction/decompilation cases pass. The isolated C# suite uses en-US; upstream's fr-FR golden-output failure is addressed separately in #5. No existing assertions or generated references are changed. -Validation: ten generic Python tests and all four existing PicaVoxel extraction/decompilation tests pass. Two additional local shaders were exported directly from the same game bundle with AssetStudioMod CLI v0.20.0 and AssetRipper: decompressed blobs and extracted metadata match across exporters. Those external fixtures are not included in this PR. - -With the separate emitter fixes combined, both shaders produce identical ShaderLab/HLSL/DXBC from either exporter. All four outputs pass native recompilation and deterministic DXBC execution comparisons, including serialized constant inputs. These finite interpreter checks are not GPU-rendering equivalence proofs. - -All 51 baseline C# tests pass with an en-US default culture. The unchanged golden test fails under fr-FR on upstream because of decimal separators; the numeric-formatting PR fixes that independently. No existing assertions or golden files are changed here. +Additional local validation used two external shaders exported from the same bundle with AssetStudioMod CLI v0.20.0 and AssetRipper. Their normalized metadata and decompressed blobs match. With the separate emitter fixes combined, all four exports also produced identical artifacts across exporters and passed native recompilation and finite interpreter comparisons with serialized constants. Those external fixtures are not distributed, so this additional check is not reproducible from the PR alone and does not prove GPU equivalence. diff --git a/libs/metadata.py b/libs/metadata.py index 6e3ec20..9fb4ce6 100644 --- a/libs/metadata.py +++ b/libs/metadata.py @@ -1,10 +1,10 @@ """ -Stage 0.2 — Metadata Extraction +Stage 0.2 — Metadata Extraction Pulls everything the backend (C#) needs out of the raw Unity shader JSON (the AssetStudio export) into a single metadata.json, keyed by shader name. -This does NOT touch m_CompressedBlob — that stays in blob.py/ShaderBlob. +This does NOT touch m_CompressedBlob — that stays in blob.py/ShaderBlob. """ import json @@ -162,11 +162,11 @@ def _pass(self, p, name_layouts=None): def _constant_buffers(self, p, prog_keys, index_to_name, name_layouts=None): # Unity serializes the constant-buffer layout (name + per-variable - # byte offset) in each stage's m_CommonParameters — this is the + # byte offset) in each stage's m_CommonParameters — this is the # per-pass replacement for the RDEF chunk Unity strips from shipped # bytecode. Each stage binds its OWN table (cb0 in the vertex # program can be a different buffer than cb0 in the fragment - # program), so cbuffers are keyed by (stage, slot) — never merged + # program), so cbuffers are keyed by (stage, slot) — never merged # across stages, or different buffers that share a slot number # would collide at the same byte offsets. by_key = {} @@ -226,7 +226,7 @@ def _next_slot(): # A program can bind a buffer (its name index appears in # m_ConstantBufferBindings) without listing it in - # m_ConstantBuffers — Unity only serializes the buffers the + # m_ConstantBuffers — Unity only serializes the buffers the # stage touches. When the bound name's layout is known from # another pass/stage of the same shader, reuse it so reads of # that (stage, slot) still resolve to real names. @@ -270,7 +270,7 @@ def _next_slot(): @staticmethod def _unwrap(v, default=None): # Unity wraps most scalar render state in - # {"m_Name": "...", "m_Value": N} — flatten to just N. + # {"m_Name": "...", "m_Value": N} — flatten to just N. if isinstance(v, dict) and "m_Value" in v: return v["m_Value"] return v if v is not None else default @@ -280,7 +280,7 @@ def _render_state(self, state): # like m_Culling/m_ZTest/m_ZWrite/m_RtBlend0 (values wrapped as # {"m_Value": N}). That's the schema the C# side (HlslRenderstateBuilder) # consumes. A few exports use a nested rasterState/depthState shape - # instead — fall back to that when the flat keys are absent. + # instead — fall back to that when the flat keys are absent. if not isinstance(state, dict): return {} From 390df24fbc4684c118ccf0b1adc5547756225582 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 13:00:02 +0100 Subject: [PATCH 20/93] Clarify the visible defect and scope of the review --- docs/pr-invariant-numeric-output.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/pr-invariant-numeric-output.md b/docs/pr-invariant-numeric-output.md index 08c1df3..b723563 100644 --- a/docs/pr-invariant-numeric-output.md +++ b/docs/pr-invariant-numeric-output.md @@ -1,7 +1,5 @@ # Use invariant numeric literals in shader output -ShaderLab properties, render-state offsets and HLSL constants must use decimal points regardless of the Windows locale. Preserve round-trip float precision. Regression cases exercise French and German cultures. +Shader output used the current locale, producing invalid literals such as `0,5` under fr-FR. Emit `0.5` consistently and retain round-trip precision for floats. Locale tests cover French and German formatting. Validation: 53 C# tests and all four existing PicaVoxel extraction/decompilation tests pass. - -This branch is based directly on upstream `1850608`; it does not include the other review branches. No proprietary fixtures or machine-specific paths are added. From 91bd00a0d464b8c3effeaac8de2e7df8034b3b8a Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 13:00:02 +0100 Subject: [PATCH 21/93] Clarify the visible defect and scope of the review --- docs/pr-directx-fail-fast.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/pr-directx-fail-fast.md b/docs/pr-directx-fail-fast.md index 2bc9d25..96bdcf4 100644 --- a/docs/pr-directx-fail-fast.md +++ b/docs/pr-directx-fail-fast.md @@ -1,7 +1,5 @@ # Propagate DirectX subprogram decompilation failures -A malformed DirectX program was logged as "No DXBC" and omitted from an apparently successful export. Propagate the failure with the program index and stage, retaining its original cause. A synthetic malformed program verifies the pipeline fails before writing a shader. +A corrupt DirectX subprogram was logged and omitted while the export continued. Abort with the subprogram index and original exception, and leave no final shader. This production fix is absent from upstream `1850608`; its presence on a local master branch does not make this PR redundant. Validation: 53 C# tests and all four existing PicaVoxel extraction/decompilation tests pass. C# tests use an en-US default culture; the upstream fr-FR golden-output failure is addressed independently by the numeric-formatting PR. - -This branch is based directly on upstream `1850608`; it does not include the other review branches. No proprietary fixtures or machine-specific paths are added. From e3950abdd55954616586c8ea2feac7089e3771ca Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 13:00:03 +0100 Subject: [PATCH 22/93] Clarify the visible defect and scope of the review --- docs/pr-dxbc-feature-flags.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/pr-dxbc-feature-flags.md b/docs/pr-dxbc-feature-flags.md index 0863333..c7aceac 100644 --- a/docs/pr-dxbc-feature-flags.md +++ b/docs/pr-dxbc-feature-flags.md @@ -1,7 +1,5 @@ # Read SFI0 as a 64-bit feature mask -The SFI0 reader previously treated the payload as a statistics block. Read exactly eight bytes, reject invalid lengths, and verify the feature mask against native compiler output. The existing synthetic test is corrected because its old payload represented the wrong chunk format. +`SFI0` is an eight-byte feature mask, not a statistics structure. Read its 64 bits and reject invalid chunk lengths. The corrected synthetic test and a native compiler fixture verify the actual format. Validation: 53 C# tests and all four existing PicaVoxel extraction/decompilation tests pass. C# tests use an en-US default culture; the upstream fr-FR golden-output failure is addressed independently by the numeric-formatting PR. - -This branch is based directly on upstream `1850608`; it does not include the other review branches. No proprietary fixtures or machine-specific paths are added. From 71ec780174fb5e0e48cc1a8781ae3c93df81302a Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 13:00:03 +0100 Subject: [PATCH 23/93] Clarify buffer layout contract and metadata dependency --- docs/pr-resource-binding-layouts.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/pr-resource-binding-layouts.md b/docs/pr-resource-binding-layouts.md index fc0bc48..0ed206c 100644 --- a/docs/pr-resource-binding-layouts.md +++ b/docs/pr-resource-binding-layouts.md @@ -1,7 +1,5 @@ # Preserve resource bindings and raw constant-buffer layout -Keep resources with different slots distinct even when their recovered names coincide. When a buffer needs raw fallback storage, emit one array starting at byte zero while retaining its name, binding and declared size. Native reflection tests verify the physical buffer layout. +Resources with the same recovered name could collapse despite occupying different slots. Keep bindings distinct. For partially known constant buffers, emit raw storage from byte zero while preserving the serialized extent, name and binding; native reflection tests verify that layout. The variant-metadata PR must retain the `size` field consumed here. Validation: 57 C# tests and all four existing PicaVoxel extraction/decompilation tests pass. C# tests use an en-US default culture; the upstream fr-FR golden-output failure is addressed independently by the numeric-formatting PR. - -This branch is based directly on upstream `1850608`; it does not include the other review branches. No proprietary fixtures or machine-specific paths are added. From 8c592420fe9f777d06eaf7856b72c544a90f9740 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 13:00:03 +0100 Subject: [PATCH 24/93] Clarify the visible defect and scope of the review --- docs/pr-vector-expression-widths.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/pr-vector-expression-widths.md b/docs/pr-vector-expression-widths.md index 9363f17..6e63cf0 100644 --- a/docs/pr-vector-expression-widths.md +++ b/docs/pr-vector-expression-widths.md @@ -1,7 +1,5 @@ # Preserve vector lanes in casts and dot products -Use the operand width for numeric casts and the instruction width for dot products. Keep the selected lanes when recognizing normalization; a three-lane expression must not read the fourth lane. +Vector casts could collapse to one lane, and dot-product operands could retain the wrong width. Preserve the expression width so the emitted HLSL consumes the intended components. Validation: 54 C# tests and all four existing PicaVoxel extraction/decompilation tests pass. C# tests use an en-US default culture; the upstream fr-FR golden-output failure is addressed independently by the numeric-formatting PR. The golden output was regenerated by the emitter for this change only; existing comparison assertions are unchanged. - -This branch is based directly on upstream `1850608`; it does not include the other review branches. No proprietary fixtures or machine-specific paths are added. From 8a7c610f64d283d5bbf4b9091a6092d206cf6c9b Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 13:00:03 +0100 Subject: [PATCH 25/93] Clarify the visible defect and scope of the review --- docs/pr-temporary-register-names.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/pr-temporary-register-names.md b/docs/pr-temporary-register-names.md index da2e6a2..b057dab 100644 --- a/docs/pr-temporary-register-names.md +++ b/docs/pr-temporary-register-names.md @@ -1,7 +1,5 @@ # Keep temporary register identities distinct -Recovered semantic names can be shared by different registers. Include the register identity in temporary names so one assignment cannot overwrite an unrelated value. +Name recovery could give distinct temporary registers the same HLSL identifier. Preserve register identity when choosing temporary names to prevent collisions. Regenerate the shared golden output after combining emitter changes. Validation: 52 C# tests and all four existing PicaVoxel extraction/decompilation tests pass. C# tests use an en-US default culture; the upstream fr-FR golden-output failure is addressed independently by the numeric-formatting PR. The golden output was regenerated by the emitter for this change only; existing comparison assertions are unchanged. - -This branch is based directly on upstream `1850608`; it does not include the other review branches. No proprietary fixtures or machine-specific paths are added. From 8c86088287a5008b76c15bce22fa9d653144ce82 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 13:00:03 +0100 Subject: [PATCH 26/93] Clarify the visible defect and scope of the review --- docs/pr-conditional-value-scope.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/pr-conditional-value-scope.md b/docs/pr-conditional-value-scope.md index 8a73c26..037083e 100644 --- a/docs/pr-conditional-value-scope.md +++ b/docs/pr-conditional-value-scope.md @@ -1,7 +1,5 @@ # Preserve values across conditional control flow -Keep phi copies after a closing conditional and declare branch-written values in their enclosing scope. Regression cases cover reads after a conditional and copies after a nested join. +Values assigned inside a conditional could be referenced outside their declaration scope, and phi copies could run before a nested join. Declare branch-written values in the enclosing scope and place copies after the closing conditional. Tests cover reads after an if and copies after a nested join; they do not exhaust every loop/switch combination. Validation: 53 C# tests and all four existing PicaVoxel extraction/decompilation tests pass. C# tests use an en-US default culture; the upstream fr-FR golden-output failure is addressed independently by the numeric-formatting PR. The golden output was regenerated by the emitter for this change only; existing comparison assertions are unchanged. - -This branch is based directly on upstream `1850608`; it does not include the other review branches. No proprietary fixtures or machine-specific paths are added. From edaddf91b0ba6be392e6e5c815d7e67fdf30ffa0 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 13:00:03 +0100 Subject: [PATCH 27/93] Clarify the visible defect and scope of the review --- docs/pr-explicit-texture-lod.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/pr-explicit-texture-lod.md b/docs/pr-explicit-texture-lod.md index e68415c..24ca74c 100644 --- a/docs/pr-explicit-texture-lod.md +++ b/docs/pr-explicit-texture-lod.md @@ -1,7 +1,5 @@ # Keep explicit zero texture LOD explicit -Do not replace SampleLevel(..., 0) with Sample(...): implicit derivatives can select a different mip. Remove the transformation and add a regression checking that the explicit LOD survives optimization. +An explicit texture LOD of zero was emitted as implicit sampling, changing mip selection. Preserve `SampleLevel(..., 0)` when the source instruction specifies an explicit LOD. Validation: 52 C# tests and all four existing PicaVoxel extraction/decompilation tests pass. C# tests use an en-US default culture; the upstream fr-FR golden-output failure is addressed independently by the numeric-formatting PR. The golden output was regenerated by the emitter for this change only; existing comparison assertions are unchanged. - -This branch is based directly on upstream `1850608`; it does not include the other review branches. No proprietary fixtures or machine-specific paths are added. From edbffe1220cf8a865c67fe92ce1430936c7b8f4f Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 13:03:52 +0100 Subject: [PATCH 28/93] Reject structured loads without bit-preserving temporary storage --- Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs | 4 ++++ docs/pr-structured-buffer-loads.md | 2 +- tests/UnityShaderDecompiler.Tests/RegisterRoundTrip.cs | 8 ++++---- .../StructuredBufferLoadsTests.cs | 4 ++++ 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs b/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs index b32448e..fc95afb 100644 --- a/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs +++ b/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs @@ -2801,6 +2801,10 @@ private static string RenderTextureOp(IRExpression.TextureOperationExpression te private static string RenderStructuredLoad(IRExpression.TextureOperationExpression tex, PrintContext ctx) { + // Float temporaries can numerically convert integer outputs and lose payload bits. + if (!ctx.RawTemporaryStorage) + throw new InvalidDataException("Structured loads require --no-fuse-temps to preserve register bits."); + if (tex.Coordinates is null || tex.Offset is not IRExpression.ConstantExpression { RawValues.Length: 1 } offset) throw new InvalidDataException("Structured load requires an element index and a constant byte offset."); uint byteOffset = offset.RawValues[0]; diff --git a/docs/pr-structured-buffer-loads.md b/docs/pr-structured-buffer-loads.md index 3637f96..adf1644 100644 --- a/docs/pr-structured-buffer-loads.md +++ b/docs/pr-structured-buffer-loads.md @@ -4,6 +4,6 @@ Structured loads previously combined the element index with a byte offset and as Depends on #7 (`fix/raw-register-semantics`), now included in this branch. Native tests exposed numeric float-to-integer conversions in the standalone version: raw register transport is required to preserve integer output bits. Review the structured-load changes against that branch, and merge #7 first. -Three native round trips cover float, uint and int outputs, a 32-byte stride, a nonzero member offset, swizzling and two elements. A restricted bytecode evaluator checks exact words, including NaN payloads, subnormals and signed zero; unexpected conversions fail. The integer cases fail without #7 and pass with it. These checks use `fuseTemps: false`; they do not establish fused-mode or GPU execution equivalence. +Three native round trips cover float, uint and int outputs, a 32-byte stride, a nonzero member offset, swizzling and two elements. A restricted bytecode evaluator checks exact words, including NaN payloads, subnormals and signed zero; unexpected conversions fail. The integer cases fail without #7 and pass with it. Structured loads require `--no-fuse-temps`; fused mode now fails explicitly instead of risking numeric conversion. All three native cases verify that rejection as well as the successful bit-preserving path. These checks do not prove GPU execution equivalence. Validation: 65 C# tests and all four PicaVoxel end-to-end cases pass on Windows. The isolated suite uses en-US because upstream's fr-FR golden-output failure is addressed separately in #5. diff --git a/tests/UnityShaderDecompiler.Tests/RegisterRoundTrip.cs b/tests/UnityShaderDecompiler.Tests/RegisterRoundTrip.cs index 16b3487..5d24407 100644 --- a/tests/UnityShaderDecompiler.Tests/RegisterRoundTrip.cs +++ b/tests/UnityShaderDecompiler.Tests/RegisterRoundTrip.cs @@ -22,14 +22,14 @@ internal static DxbcFile Compile(string source, string entry = "main") return file; } - internal static DxbcFile Recompile(DxbcFile original) + internal static DxbcFile Recompile(DxbcFile original, bool fuseTemps = false) { - IRPipeline.Result pipeline = IRPipeline.Run(original, preserveArithmetic: true); + IRPipeline.Result pipeline = IRPipeline.Run(original, preserveArithmetic: !fuseTemps); Assert.True(pipeline.SsaVerification.IsValid); HlslFunctionNode function = HlslAstBuilder.BuildFunction("RoundTrip", ShaderGpuProgramType.DX11PixelSM50, pipeline.Program.Declarations, pipeline.Blocks, original.InputSignature, original.OutputSignature)!; - function.RawTemporaryStorage = true; + function.RawTemporaryStorage = !fuseTemps; HlslPassNode pass = new HlslPassNode { FragmentFunction = function }; pass.Structs.Add(function.InputStruct!); pass.Structs.Add(function.OutputStruct!); @@ -39,7 +39,7 @@ internal static DxbcFile Recompile(DxbcFile original) subShader.Passes.Add(pass); HlslShaderNode shader = new HlslShaderNode { Name = "Test/RegisterRoundTrip" }; shader.SubShaders.Add(subShader); - string shaderText = HlslPrettyPrinter.Print(shader, fuseTemps: false); + string shaderText = HlslPrettyPrinter.Print(shader, fuseTemps: fuseTemps); string hlsl = Regex.Match(shaderText, @"HLSLPROGRAM(.*?)ENDHLSL", RegexOptions.Singleline).Groups[1].Value; return Compile(hlsl, function.Name); } diff --git a/tests/UnityShaderDecompiler.Tests/StructuredBufferLoadsTests.cs b/tests/UnityShaderDecompiler.Tests/StructuredBufferLoadsTests.cs index 59bf329..4fd7253 100644 --- a/tests/UnityShaderDecompiler.Tests/StructuredBufferLoadsTests.cs +++ b/tests/UnityShaderDecompiler.Tests/StructuredBufferLoadsTests.cs @@ -27,6 +27,9 @@ public void StructuredLoadPreservesWordsThroughNativeRoundTrip(string type) "{ return data[index].value.zxyw; }"; Parser.DXBC.DxbcFile original = RegisterRoundTrip.Compile(source); Assert.Contains(original.Shader!.Instructions, instruction => instruction.Opcode == Opcode.LdStructured); + System.IO.InvalidDataException error = Assert.Throws( + () => RegisterRoundTrip.Recompile(original, fuseTemps: true)); + Assert.Contains("--no-fuse-temps", error.Message); Parser.DXBC.DxbcFile rebuilt = RegisterRoundTrip.Recompile(original); Assert.Equal(original.OutputSignature!.Elements[0].ComponentType, rebuilt.OutputSignature!.Elements[0].ComponentType); @@ -106,6 +109,7 @@ public void StructuredBufferDeclarationUsesItsActualStride() private static string Print(HlslFunctionNode function) { + function.RawTemporaryStorage = true; HlslShaderNode shader = new HlslShaderNode { Name = "Test/Scope" }; HlslSubShaderNode subShader = new HlslSubShaderNode(); subShader.Passes.Add(new HlslPassNode { FragmentFunction = function }); From ec7936d3b35e7165e7e0ece485d78f8d50add443 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 13:06:00 +0100 Subject: [PATCH 29/93] Document invariant numeric output in the README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b646a84..ec2d082 100644 --- a/README.md +++ b/README.md @@ -242,7 +242,8 @@ Each subprogram goes through `IRPipeline` (`DXBC/IR/IRPipeline.cs`): marks a pass boundary. Vertex and fragment variants are paired by interpolator hand-off signature, and byte-identical pairs are deduplicated. 13. **Pretty print** — emit properties, tags, LOD, render state, - `HLSLPROGRAM` blocks, structs, resources, and function bodies. + `HLSLPROGRAM` blocks, structs, resources, and function bodies. Numeric literals + use a decimal point regardless of system locale (for example, `0.5`, not `0,5`). 14. **Surface-shader recognition** — rewrite a pass to `#pragma surface` form when it carries the compiled signature of a surface shader. From 0051df18c909100a458bc5cea23d3a7a12f54c8e Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 13:06:00 +0100 Subject: [PATCH 30/93] Document metadata offset schemas in the README --- README.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b646a84..dd66e27 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,8 @@ **Turn compiled Unity shaders back into readable ShaderLab and HLSL.** Unity ships shaders as LZ4-compressed blobs of stripped D3D11 DXBC bytecode. -This project takes the JSON that [AssetStudio](https://github.com/Perfare/AssetStudio) -exports for a `Shader` asset, recovers the serialized ShaderLab metadata, and +This project accepts `Shader` JSON exports from [AssetStudio](https://github.com/Perfare/AssetStudio) +or AssetRipper, recovers the serialized ShaderLab metadata, and decompiles every shipped GPU program through a typed SSA IR into per-pass HLSL. > Not affiliated with, sponsored by, or endorsed by Unity Technologies. @@ -103,6 +103,9 @@ pip install lz4 python Extract.py shaders_json/HOLO_Holo.json ``` +Extraction accepts legacy and current parameter-offset fields without rewriting +the source JSON. Missing or invalid offsets and conflicting schema aliases fail explicitly. + `Extract.py` writes `Output/HOLO_Holo/`: | File | Contents | @@ -159,7 +162,7 @@ the project directory. | Argument | Default | Effect | | --- | --- | --- | -| `json_path` (positional) | — | AssetStudio JSON export to process | +| `json_path` (positional) | — | AssetStudio or AssetRipper Shader JSON export to process | | `--dummy-dir` | `shaders_dummy` | Directory to search for the matching `.shader` reference | | `--out-dir` | `Output` | Root output directory | | `--flat` | off | Write the three files directly into `--out-dir` instead of a per-shader subfolder | @@ -193,7 +196,7 @@ and spaces are stripped). ## How it works ``` -AssetStudio JSON export +AssetStudio / AssetRipper JSON export └─ Stage 0: base64 + LZ4 → blob.bin + metadata.json └─ Stage 1: DXBC → typed IR → SSA → optimize → HLSL AST → .shader ``` From 11ef5ba17e10cd6252977189ff49dc3447cd027c Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 13:06:01 +0100 Subject: [PATCH 31/93] Document raw register semantics in the README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b646a84..57bff46 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,7 @@ the project directory. | --- | --- | | `--out-root ` | Write the `.shader` and subprogram artifacts to `` | | `--dump-stages` | Print the IR after every pipeline phase | +| `--no-fuse-temps` | Preserve raw register bits and arithmetic order; disable temporary fusion | | `--save-subprograms` | Write per-subprogram `.bin`, `.dxbc`, and disassembly files | | `--no-surface-shaders` | Emit raw HLSLPROGRAM passes instead of surface-shader form | | `--keep-passes` | Keep original passes as comments when a surface shader is recognized | @@ -176,7 +177,7 @@ and spaces are stripped). | `--dump-stages` | Print a full IR snapshot after every pipeline phase and every optimization pass | | `--dump-keywords` | Print each subprogram's global and local keyword sets | | `--save-subprograms` | Also write per-subprogram `program{i}.bin`, `program{i}.dxbc`, and `program{i}.hlsl` (disassembly) | -| `--no-fuse-temps` | Disable temp fusion; keep every SSA copy as a separate statement | +| `--no-fuse-temps` | Keep raw uint temporary storage with typed reads and outputs; disable temporary fusion and arithmetic rewrites | | `--no-surface-shaders` | Disable surface-shader recognition; emit raw reconstructed HLSLPROGRAM passes | | `--keep-passes` | When a pass is recognized as a surface shader, keep the original compiled passes as comments | | `--keep-register-bindings` | Keep explicit `register(tN)` / `register(bN)` bindings (stripped by default) | From 76048771d63fa58ff505a39eb40bcbfc46dbe33e Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 13:06:01 +0100 Subject: [PATCH 32/93] Document directx fail fast in the README --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index b646a84..4a4d0af 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,9 @@ dotnet run --project "Shader Decompiler/Shader Decompiler.csproj" -- Output/HOLO The result is written to `Shader Decompiler/Output/HOLO_Holo.shader`. +A DirectX subprogram failure aborts decompilation with its index and cause, +instead of silently omitting it from the final shader. + ## Usage `Shader Decompiler` accepts a Stage 0 output folder or a `blob.bin` with a From c1c4217b67f39198d06a78592dc8694066de0c96 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 13:06:01 +0100 Subject: [PATCH 33/93] Document dxbc feature flags in the README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b646a84..1cb6258 100644 --- a/README.md +++ b/README.md @@ -219,7 +219,8 @@ Each subprogram goes through `IRPipeline` (`DXBC/IR/IRPipeline.cs`): DXBC container. 2. **Container parse** — validate the `DXBC` magic and lengths; parse `ISGN`, `OSGN`, `OSG5`, `PSGN`, `PCON`, `SFI0`, `RDEF`, `STAT`, and the - `SHDR`/`SHEX` shader chunk. Unknown chunks are preserved raw. + `SHDR`/`SHEX` shader chunk. `SFI0` is read as a 64-bit feature mask; invalid + lengths are rejected. Unknown chunks are preserved raw. 3. **Instruction decode** — decode every SM4/SM5 instruction and operand. 4. **IR build** — build flat, typed IR statements. 5. **CFG** — build basic blocks and edges. From 3af5469f9b4d532f74bad46c4a3a959e2bcf1d38 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 13:06:02 +0100 Subject: [PATCH 34/93] Document structured buffer loads in the README --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 57bff46..e2a8ac6 100644 --- a/README.md +++ b/README.md @@ -316,6 +316,9 @@ root, prefixing each with ## Limitations - Targets Unity 2022.3 D3D11 shader blobs. +- Structured-buffer loads require `--no-fuse-temps`. Element stride, constant + byte offsets and swizzles are preserved; fused mode, dynamic offsets and + unaligned or out-of-range accesses fail explicitly. - Vertex and fragment stages are fully reconstructed. Hull, geometry, domain, and compute programs are parsed and IR-built, but the AST/emit path mainly targets vertex/fragment; other stages attach by serialization order. From e96ab7f67a40cac3dacf1b7345f532bdb8d6525b Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 13:06:15 +0100 Subject: [PATCH 35/93] Document resource binding layouts in the README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index b646a84..64a5727 100644 --- a/README.md +++ b/README.md @@ -235,6 +235,8 @@ Each subprogram goes through `IRPipeline` (`DXBC/IR/IRPipeline.cs`): the matrix, vector, texture, loop, and shader-pattern recognizers. 9. **Leave SSA** — eliminate phis and coalesce copies. 10. **Metadata binding** — attach RDEF/ISGN/OSGN symbolic names to the IR. + Same-named resources keep distinct binding slots; raw constant-buffer fallback + storage preserves the serialized extent and starts at byte zero. 11. **AST build** — one function node per subprogram; the Shader/SubShader/Pass shell comes from `metadata.json`. 12. **Pass and variant recovery** — `BuildVariantPasses` groups subprograms From ef4cf63249184b381e26002bc5617c3a31ce1ad6 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 13:06:16 +0100 Subject: [PATCH 36/93] Document vector expression widths in the README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b646a84..2d4c193 100644 --- a/README.md +++ b/README.md @@ -231,6 +231,7 @@ Each subprogram goes through `IRPipeline` (`DXBC/IR/IRPipeline.cs`): motion, sparse conditional constant propagation) plus a CFG inner loop (branch simplification, CFG cleanup, phi simplification), up to 50 iterations. + Vector casts and dot products retain the required component widths. 8. **Pattern recognition** — fold shader idioms back into high-level IR with the matrix, vector, texture, loop, and shader-pattern recognizers. 9. **Leave SSA** — eliminate phis and coalesce copies. From 70291a028fe223e5f9ea9f082094b28665b20b32 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 13:06:16 +0100 Subject: [PATCH 37/93] Document temporary register names in the README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b646a84..cf137af 100644 --- a/README.md +++ b/README.md @@ -235,6 +235,7 @@ Each subprogram goes through `IRPipeline` (`DXBC/IR/IRPipeline.cs`): the matrix, vector, texture, loop, and shader-pattern recognizers. 9. **Leave SSA** — eliminate phis and coalesce copies. 10. **Metadata binding** — attach RDEF/ISGN/OSGN symbolic names to the IR. + Temporary names retain register identity to avoid collisions. 11. **AST build** — one function node per subprogram; the Shader/SubShader/Pass shell comes from `metadata.json`. 12. **Pass and variant recovery** — `BuildVariantPasses` groups subprograms From 85980af08656a2c2ca20cd11b749817a9286bcb0 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 13:06:16 +0100 Subject: [PATCH 38/93] Document conditional value scope in the README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b646a84..5c3c6c5 100644 --- a/README.md +++ b/README.md @@ -233,7 +233,8 @@ Each subprogram goes through `IRPipeline` (`DXBC/IR/IRPipeline.cs`): iterations. 8. **Pattern recognition** — fold shader idioms back into high-level IR with the matrix, vector, texture, loop, and shader-pattern recognizers. -9. **Leave SSA** — eliminate phis and coalesce copies. +9. **Leave SSA** — eliminate phis and coalesce copies. Copies follow their + conditional join; branch-written values remain in scope for subsequent reads. 10. **Metadata binding** — attach RDEF/ISGN/OSGN symbolic names to the IR. 11. **AST build** — one function node per subprogram; the Shader/SubShader/Pass shell comes from `metadata.json`. From 71acd532ce9f456e1b74c3b341a5dc1e64972b8d Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 13:06:17 +0100 Subject: [PATCH 39/93] Document serialized variant pairing in the README --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b646a84..e7b01bc 100644 --- a/README.md +++ b/README.md @@ -237,10 +237,10 @@ Each subprogram goes through `IRPipeline` (`DXBC/IR/IRPipeline.cs`): 10. **Metadata binding** — attach RDEF/ISGN/OSGN symbolic names to the IR. 11. **AST build** — one function node per subprogram; the Shader/SubShader/Pass shell comes from `metadata.json`. -12. **Pass and variant recovery** — `BuildVariantPasses` groups subprograms - into passes. Unity serializes them pass-by-pass, so a stage regression - marks a pass boundary. Vertex and fragment variants are paired by - interpolator hand-off signature, and byte-identical pairs are deduplicated. +12. **Pass and variant recovery** — use serialized program references when + available, pairing stages whose enabled keywords agree within their shared + keyword masks. Validate stage interfaces; older metadata falls back to + serialization order and signatures. Byte-identical pairs are deduplicated. 13. **Pretty print** — emit properties, tags, LOD, render state, `HLSLPROGRAM` blocks, structs, resources, and function bodies. 14. **Surface-shader recognition** — rewrite a pass to `#pragma surface` form @@ -318,6 +318,8 @@ root, prefixing each with - Vertex and fragment stages are fully reconstructed. Hull, geometry, domain, and compute programs are parsed and IR-built, but the AST/emit path mainly targets vertex/fragment; other stages attach by serialization order. +- Compatible keyword variants are emitted as separate passes. Runtime keyword + selection is not implemented. - Surface-shader rewrites are gated on recompiled signature matches. Passes that do not match are emitted as HLSLPROGRAM. - Passes declared in metadata with no compiled DX11 variants in the blob are From c2dd84f6ad5fde16f51f3e35d289a6d9b8798050 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 13:06:17 +0100 Subject: [PATCH 40/93] Document explicit texture lod in the README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b646a84..dae79fc 100644 --- a/README.md +++ b/README.md @@ -233,6 +233,7 @@ Each subprogram goes through `IRPipeline` (`DXBC/IR/IRPipeline.cs`): iterations. 8. **Pattern recognition** — fold shader idioms back into high-level IR with the matrix, vector, texture, loop, and shader-pattern recognizers. + Explicit texture LODs, including zero, remain explicit in emitted HLSL. 9. **Leave SSA** — eliminate phis and coalesce copies. 10. **Metadata binding** — attach RDEF/ISGN/OSGN symbolic names to the IR. 11. **AST build** — one function node per subprogram; the From d8603ec26c6bda9463e8cdd11e9d9dd79f8f7cb6 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 13:06:41 +0100 Subject: [PATCH 41/93] Place vector-width behavior under HLSL emission --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2d4c193..8f399c3 100644 --- a/README.md +++ b/README.md @@ -231,7 +231,6 @@ Each subprogram goes through `IRPipeline` (`DXBC/IR/IRPipeline.cs`): motion, sparse conditional constant propagation) plus a CFG inner loop (branch simplification, CFG cleanup, phi simplification), up to 50 iterations. - Vector casts and dot products retain the required component widths. 8. **Pattern recognition** — fold shader idioms back into high-level IR with the matrix, vector, texture, loop, and shader-pattern recognizers. 9. **Leave SSA** — eliminate phis and coalesce copies. @@ -244,6 +243,7 @@ Each subprogram goes through `IRPipeline` (`DXBC/IR/IRPipeline.cs`): interpolator hand-off signature, and byte-identical pairs are deduplicated. 13. **Pretty print** — emit properties, tags, LOD, render state, `HLSLPROGRAM` blocks, structs, resources, and function bodies. + Vector casts and dot-product operands keep their required component widths. 14. **Surface-shader recognition** — rewrite a pass to `#pragma surface` form when it carries the compiled signature of a surface shader. From 894487167398331907704b0393a194c26a51cf91 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 13:12:44 +0100 Subject: [PATCH 42/93] Consolidate register semantics and structured loads into one review --- docs/pr-raw-register-semantics.md | 7 ------- docs/pr-structured-buffer-loads.md | 10 +++++----- 2 files changed, 5 insertions(+), 12 deletions(-) delete mode 100644 docs/pr-raw-register-semantics.md diff --git a/docs/pr-raw-register-semantics.md b/docs/pr-raw-register-semantics.md deleted file mode 100644 index 1f800a7..0000000 --- a/docs/pr-raw-register-semantics.md +++ /dev/null @@ -1,7 +0,0 @@ -# Preserve register bits and arithmetic in no-fuse mode - -With `--no-fuse-temps`, interpreting temporary registers as numeric floats can corrupt integer moves and comparison masks. Preserve raw uint storage with typed reads and outputs; for example, `0x01000001` must remain that exact integer instead of rounding to `0x01000000`. Integer negation applies after bit interpretation, and arithmetic rewrites must not remove the addition before fractional-part evaluation. - -Native round-trip tests cover `mov` and `movc` to signed and unsigned outputs, both conditional outcomes, and bit patterns representing NaNs, subnormals and signed zero. A restricted bytecode evaluator compares exact output words against explicit expectations and rejects unexpected operations. Disabling bit preservation makes all four new cases fail. These are compiler/bytecode checks, not GPU execution tests. - -Validation: 58 C# tests and four PicaVoxel end-to-end cases pass on Windows. The isolated suite uses en-US because upstream's fr-FR golden-output failure is fixed separately in #5. Existing upstream assertions are unchanged. diff --git a/docs/pr-structured-buffer-loads.md b/docs/pr-structured-buffer-loads.md index adf1644..14e2ba7 100644 --- a/docs/pr-structured-buffer-loads.md +++ b/docs/pr-structured-buffer-loads.md @@ -1,9 +1,9 @@ -# Preserve structured resource strides and load offsets +# Preserve register bits and structured-buffer loads -Structured loads previously combined the element index with a byte offset and assumed a fixed element layout. Preserve the declared stride, the byte offset and the resource swizzle independently; reject unaligned or out-of-range accesses. +Float temporary storage can corrupt integer moves and structured-buffer results. With `--no-fuse-temps`, preserve raw uint storage, typed reads and outputs, full-bit comparison masks and integer source modifiers. Keep arithmetic rewrites from removing the addition before fractional-part evaluation. -Depends on #7 (`fix/raw-register-semantics`), now included in this branch. Native tests exposed numeric float-to-integer conversions in the standalone version: raw register transport is required to preserve integer output bits. Review the structured-load changes against that branch, and merge #7 first. +Structured loads preserve element stride, byte offset and resource swizzle independently. These changes belong together because correct addressing alone still loses integer output bits without raw register transport. Structured loads require `--no-fuse-temps`; fused mode, dynamic offsets and unaligned or out-of-range accesses fail explicitly. This PR includes the former #7; no separate dependency remains. -Three native round trips cover float, uint and int outputs, a 32-byte stride, a nonzero member offset, swizzling and two elements. A restricted bytecode evaluator checks exact words, including NaN payloads, subnormals and signed zero; unexpected conversions fail. The integer cases fail without #7 and pass with it. Structured loads require `--no-fuse-temps`; fused mode now fails explicitly instead of risking numeric conversion. All three native cases verify that rejection as well as the successful bit-preserving path. These checks do not prove GPU execution equivalence. +Native round trips cover signed/unsigned `mov` and `movc`, plus float/uint/int structured loads with stride, offset and swizzle. A restricted bytecode evaluator checks exact output words, including NaN payloads, subnormals and signed zero, and rejects unexpected operations. Regression checks also verify rejection of fused structured loads. These are compiler/bytecode checks, not GPU execution tests. -Validation: 65 C# tests and all four PicaVoxel end-to-end cases pass on Windows. The isolated suite uses en-US because upstream's fr-FR golden-output failure is addressed separately in #5. +Validation: 65 C# tests and all four PicaVoxel end-to-end cases pass on Windows. The isolated suite uses en-US because upstream's fr-FR golden-output failure is addressed separately in #5. The README documents both raw register preservation and the structured-load restriction. From fcbd4dad2548d76d7a8098e54af72fdd214781bf Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 13:33:34 +0100 Subject: [PATCH 43/93] Restore runtime keyword selection within serialized passes --- README.md | 10 +- Shader Decompiler/Core/Hlsl/HlslAST.cs | 3 + .../Core/Hlsl/HlslPrettyPrinter.cs | 55 +++++++-- .../DXBC/Metadata/Shadermetadata.cs | 5 + Shader Decompiler/Decompiler.cs | 63 +++++++++- Shader Decompiler/Program.cs | 4 +- Shader Decompiler/RuntimeVariantManifest.cs | 89 ++++++++++++++ Shader Decompiler/RuntimeVariantPlan.cs | 84 +++++++++++++ Shader Decompiler/Tests/RecompileVerify.cs | 7 +- Shader Decompiler/VariantPairing.cs | 24 ++++ libs/metadata.py | 16 +++ tests/UnityRuntime/README.md | 22 ++++ tests/UnityRuntime/RuntimeKeywordChecks.cs | 91 ++++++++++++++ .../RuntimeVariantEmissionTests.cs | 115 ++++++++++++++++++ .../RuntimeVariantPlanTests.cs | 108 ++++++++++++++++ .../VariantInterfaceTests.cs | 25 ++++ tests/test_keyword_flags.py | 24 ++++ 17 files changed, 728 insertions(+), 17 deletions(-) create mode 100644 Shader Decompiler/RuntimeVariantManifest.cs create mode 100644 Shader Decompiler/RuntimeVariantPlan.cs create mode 100644 tests/UnityRuntime/README.md create mode 100644 tests/UnityRuntime/RuntimeKeywordChecks.cs create mode 100644 tests/UnityShaderDecompiler.Tests/RuntimeVariantEmissionTests.cs create mode 100644 tests/UnityShaderDecompiler.Tests/RuntimeVariantPlanTests.cs create mode 100644 tests/UnityShaderDecompiler.Tests/VariantInterfaceTests.cs create mode 100644 tests/test_keyword_flags.py diff --git a/README.md b/README.md index e7b01bc..9695d48 100644 --- a/README.md +++ b/README.md @@ -318,8 +318,14 @@ root, prefixing each with - Vertex and fragment stages are fully reconstructed. Hull, geometry, domain, and compute programs are parsed and IR-built, but the AST/emit path mainly targets vertex/fragment; other stages attach by serialization order. -- Compatible keyword variants are emitted as separate passes. Runtime keyword - selection is not implemented. +- By default, compatible keyword variants are emitted as separate passes. + `--restore-keyword-variants` keeps the original passes and restores D3D11 + keyword selection for complete, unambiguous vertex/fragment variant sets. + It requires serialized program references and keyword scope flags; missing, + correlated or unsupported states fail explicitly. Surface rewrites are disabled + in this mode. The adjacent `.variants.json` lets `--recompile-verify` compile + every recovered combination and rejects stale shader/manifest pairs. + [Unity GPU checks](tests/UnityRuntime/README.md) target **6000.0.75f1**. - Surface-shader rewrites are gated on recompiled signature matches. Passes that do not match are emitted as HLSLPROGRAM. - Passes declared in metadata with no compiled DX11 variants in the blob are diff --git a/Shader Decompiler/Core/Hlsl/HlslAST.cs b/Shader Decompiler/Core/Hlsl/HlslAST.cs index 2a4be75..12506f1 100644 --- a/Shader Decompiler/Core/Hlsl/HlslAST.cs +++ b/Shader Decompiler/Core/Hlsl/HlslAST.cs @@ -176,6 +176,9 @@ public sealed class HlslRenderState public sealed class HlslPassNode { + internal RuntimeVariantPlan? RuntimeVariants { get; set; } + internal List VariantPrograms { get; } = new(); + internal string ShaderTarget { get; set; } = "4.0"; public List DirectXPrograms { get; } = new(); public string Name { get; init; } = ""; public Dictionary Tags { get; } = new(); diff --git a/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs b/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs index 222df18..a4a1eb5 100644 --- a/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs +++ b/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs @@ -167,14 +167,55 @@ private static void PrintPass(StringBuilder sb, HlslPassNode pass, bool fuseTemp PrintTags(sb, pass.Tags, indent: " "); PrintRenderState(sb, pass.State); + sb.Append(" HLSLPROGRAM\n"); + if (pass.RuntimeVariants is RuntimeVariantPlan plan) + { + sb.Append(" #pragma only_renderers d3d11\n"); + sb.Append($" #pragma target {pass.ShaderTarget}\n"); + sb.Append(" #pragma vertex vert\n"); + sb.Append(" #pragma fragment frag\n"); + foreach (RuntimeKeywordGroup group in plan.Groups) + { + string directive = group.Local ? "multi_compile_local" : "multi_compile"; + string choices = (group.AllowsDisabled ? "__ " : "") + string.Join(" ", group.Keywords); + sb.Append($" #pragma {directive} {choices}\n"); + } + foreach (string keyword in plan.StrippedKeywords) + { + string directive = plan.LocalKeywords.Contains(keyword) ? "multi_compile_local" : "multi_compile"; + sb.Append($" #pragma {directive} __ {keyword}\n"); + sb.Append($" #pragma skip_variants {keyword}\n"); + } + for (int index = 0; index < plan.States.Count; index++) + { + HashSet enabled = plan.States[index].Keywords.ToHashSet(StringComparer.Ordinal); + string condition = string.Join(" && ", plan.Mask.Select(keyword => + (enabled.Contains(keyword) ? "" : "!") + $"defined({keyword})")); + if (condition.Length == 0) condition = "1"; + sb.Append($" #{(index == 0 ? "if" : "elif")} {condition}\n"); + PrintPassProgram(sb, pass.VariantPrograms[index], fuseTemps, stripRegisterBindings, false); + } + sb.Append(" #else\n"); + sb.Append(" #error No serialized program matches this shader keyword state\n"); + sb.Append(" #endif\n"); + } + else + { + PrintPassProgram(sb, pass, fuseTemps, stripRegisterBindings, true); + } + sb.Append(" ENDHLSL\n"); + sb.Append(" }\n"); + } + + private static void PrintPassProgram(StringBuilder sb, HlslPassNode pass, bool fuseTemps, + bool stripRegisterBindings, bool entryPragmas) + { // Empty pass (no decompiled functions) — emit a valid stub so // pass indices stay stable for Graphics.Blit(material, passIndex). bool hasAnyFunction = pass.VertexFunction is not null || pass.FragmentFunction is not null || pass.GeometryFunction is not null || pass.HullFunction is not null || pass.DomainFunction is not null || pass.ComputeFunction is not null; - sb.Append(" HLSLPROGRAM\n"); - if (!hasAnyFunction) { sb.AppendLine(" #pragma vertex vert"); @@ -188,13 +229,13 @@ private static void PrintPass(StringBuilder sb, HlslPassNode pass, bool fuseTemp // Interpolator hand-off map: the vertex stage records which semantic // fields carry named values (worldPos, worldNormal, ...) so the // fragment stage can seed the matching input registers. - var stageInterpolators = new Dictionary(); + Dictionary stageInterpolators = new Dictionary(); // Build cross-stage cbuffer variable rename mapping: when vert and // frag have different variables at the same byte offset within a // merged cbuffer, one variable is declared and the other's references // in the function body are redirected. - var cbufferRenames = BuildCbufferRenameMap(pass); + Dictionary cbufferRenames = BuildCbufferRenameMap(pass); foreach (HlslResourceNode res in pass.Resources) PrintResource(sb, res, stripRegisterBindings, cbufferRenames); @@ -204,20 +245,18 @@ private static void PrintPass(StringBuilder sb, HlslPassNode pass, bool fuseTemp if (pass.VertexFunction is not null) { - sb.Append(" #pragma vertex vert\n"); + if (entryPragmas) sb.Append(" #pragma vertex vert\n"); PrintFunction(sb, pass.VertexFunction, pass.Cbuffers, fuseTemps, stageOutputs: stageInterpolators, resources: pass.Resources, cbufferRenames: cbufferRenames); } if (pass.FragmentFunction is not null) { - sb.Append(" #pragma fragment frag\n"); + if (entryPragmas) sb.Append(" #pragma fragment frag\n"); PrintFunction(sb, pass.FragmentFunction, pass.Cbuffers, fuseTemps, stageInputs: stageInterpolators, resources: pass.Resources, cbufferRenames: cbufferRenames); } foreach (HlslFunctionNode? f in new[] { pass.GeometryFunction, pass.HullFunction, pass.DomainFunction, pass.ComputeFunction }) if (f is not null) PrintFunction(sb, f, pass.Cbuffers, fuseTemps, resources: pass.Resources, cbufferRenames: cbufferRenames); - sb.Append(" ENDHLSL\n"); - sb.Append(" }\n"); } private static void PrintRenderState(StringBuilder sb, HlslRenderState s) diff --git a/Shader Decompiler/DXBC/Metadata/Shadermetadata.cs b/Shader Decompiler/DXBC/Metadata/Shadermetadata.cs index 2e1c0c3..9fcaeec 100644 --- a/Shader Decompiler/DXBC/Metadata/Shadermetadata.cs +++ b/Shader Decompiler/DXBC/Metadata/Shadermetadata.cs @@ -19,6 +19,7 @@ public sealed class ShaderMetadata public string Fallback { get; init; } = ""; public string CustomEditor { get; init; } = ""; public List Keywords { get; init; } = new(); + public Dictionary KeywordFlags { get; } = new(StringComparer.Ordinal); public List Dependencies { get; init; } = new(); public JsonElement Raw { get; init; } @@ -43,6 +44,10 @@ public static ShaderMetadata Load(string path) meta.Keywords.Add(k.GetString() ?? ""); } + if (root.TryGetProperty("keywordFlags", out JsonElement keywordFlags)) + foreach (JsonProperty keyword in keywordFlags.EnumerateObject()) + meta.KeywordFlags.Add(keyword.Name, keyword.Value.GetInt32()); + if (root.TryGetProperty("properties", out var props) && props.ValueKind == JsonValueKind.Array) { foreach (var p in props.EnumerateArray()) diff --git a/Shader Decompiler/Decompiler.cs b/Shader Decompiler/Decompiler.cs index ee453e7..8144ca6 100644 --- a/Shader Decompiler/Decompiler.cs +++ b/Shader Decompiler/Decompiler.cs @@ -20,6 +20,9 @@ namespace Parser; /// public sealed class DecompileOptions { + /// Keep serialized passes and select their programs through Unity keywords. + public bool RestoreKeywordVariants { get; init; } + /// Print the IR after every pipeline phase. public bool DumpStages { get; init; } @@ -352,7 +355,10 @@ public static DecompilationResult Decompile( // Stage 2b — recover the pass each candidate belongs to from the // serialization order and emit one pass per distinct compiled // (vertex, fragment) variant pair. - BuildVariantPasses(astShader, allPasses, candidates, log); + if (options.RestoreKeywordVariants) + BuildRuntimeVariantPasses(astShader, allPasses, candidates, project.Metadata.KeywordFlags, log); + else + BuildVariantPasses(astShader, allPasses, candidates, log); log.WriteLine(); log.WriteLine("HLSL AST (Stage 2)"); @@ -375,7 +381,7 @@ public static DecompilationResult Decompile( // original HLSLPROGRAM passes are kept as a comment only with // --keep-passes; by default the output is just the reconstructed // source so surface shaders match their original line count. - if (options.ReconstructSurfaceShaders) + if (options.ReconstructSurfaceShaders && !options.RestoreKeywordVariants) { string? reconstructed = HlslSurfaceShaderRecognizer.TryReconstruct( text, project.Metadata, options.KeepPasses); @@ -390,6 +396,8 @@ public static DecompilationResult Decompile( if (options.OutRoot is { } outRoot) { outputPath = Path.Combine(outRoot, SafeFileName(astShader.Name) + ".shader"); + if (options.RestoreKeywordVariants) + RuntimeVariantManifest.Write(outputPath, text, astShader, candidates); File.WriteAllText(outputPath, text); log.WriteLine(); log.WriteLine($"Stage 13/14: full shader written to {outputPath} ({text.Length} chars)"); @@ -614,9 +622,8 @@ private static void BuildSerializedVariantPasses( throw new InvalidDataException("Serialized variant references a missing DXBC program."); if (vertex.Stage != HlslShaderStage.Vertex || fragment.Stage != HlslShaderStage.Fragment) throw new InvalidDataException("Serialized variant stage does not match its DXBC program."); - string[] inputs = fragment.InputInterpolators.Split(';', StringSplitOptions.RemoveEmptyEntries); - string[] outputs = vertex.OutputInterpolators.Split(';', StringSplitOptions.RemoveEmptyEntries); - if (!inputs.All(outputs.Contains)) + if (vertex.Dxbc.OutputSignature is null || fragment.Dxbc.InputSignature is null + || !VariantPairing.InterfacesMatch(vertex.Dxbc.OutputSignature.Elements, fragment.Dxbc.InputSignature.Elements)) throw new InvalidDataException($"Variant {vertexIndex}/{fragmentIndex} has incompatible interpolators."); HlslPassNode pass = insertAt == parent.Passes.IndexOf(shell) ? shell : ClonePass(shell); if (!ReferenceEquals(pass, shell)) parent.Passes.Insert(insertAt, pass); @@ -632,6 +639,52 @@ private static void BuildSerializedVariantPasses( throw new InvalidDataException("DXBC programs are missing from serialized variant coverage."); } + private static void BuildRuntimeVariantPasses(HlslShaderNode shader, List shells, + List candidates, Dictionary keywordFlags, TextWriter log) + { + Dictionary byIndex = candidates.ToDictionary(c => c.Index); + HashSet covered = new HashSet(); + Dictionary textureNames = new Dictionary(); + uint textureSlot = 0; + foreach (HlslPropertyNode property in shader.Properties) + if (property.Kind == HlslPropertyKind.Texture) + textureNames[textureSlot++] = property.Name; + foreach (HlslPassNode shell in shells) + { + if (shell.DirectXPrograms.Count == 0) + throw new InvalidDataException("Runtime keyword reconstruction requires serialized program references for every pass."); + RuntimeVariantPlan plan = RuntimeVariantPlan.Build(shell.DirectXPrograms, keywordFlags); + shell.RuntimeVariants = plan; + foreach (RuntimeVariantState state in plan.States) + { + if (!byIndex.TryGetValue(state.Vertex, out DecompiledSubprogram? vertex) + || !byIndex.TryGetValue(state.Fragment, out DecompiledSubprogram? fragment)) + throw new InvalidDataException("Serialized variant references a missing DXBC program."); + if (vertex.Stage != HlslShaderStage.Vertex || fragment.Stage != HlslShaderStage.Fragment) + throw new InvalidDataException("Serialized variant stage does not match its DXBC program."); + if (vertex.Dxbc.OutputSignature is null || fragment.Dxbc.InputSignature is null + || !VariantPairing.InterfacesMatch(vertex.Dxbc.OutputSignature.Elements, fragment.Dxbc.InputSignature.Elements)) + throw new InvalidDataException($"Variant {state.Vertex}/{state.Fragment} has incompatible interpolators."); + uint major = Math.Max(vertex.Dxbc.Shader!.MajorVersion, fragment.Dxbc.Shader!.MajorVersion); + if (major is not (4 or 5)) + throw new InvalidDataException("Runtime keyword reconstruction requires shader model 4 or 5."); + string target = major == 5 ? "5.0" : "4.0"; + if (shell.VariantPrograms.Count != 0 && shell.ShaderTarget != target) + throw new InvalidDataException("Runtime variants with different shader targets are not supported."); + shell.ShaderTarget = target; + HlslPassNode leaf = ClonePass(shell); + AttachPassProgram(leaf, vertex, textureNames, log); + AttachPassProgram(leaf, fragment, textureNames, log); + shell.VariantPrograms.Add(leaf); + covered.Add(state.Vertex); + covered.Add(state.Fragment); + } + log.WriteLine($"Runtime variants: {plan.States.Count} keyword states in one serialized pass."); + } + if (byIndex.Keys.Except(covered).Any()) + throw new InvalidDataException("DXBC programs are missing from serialized variant coverage."); + } + // Attaches one candidate's function, resources and structs to a pass, // unioning cbuffer members the way the previous streaming loop did. private static void AttachPassProgram( diff --git a/Shader Decompiler/Program.cs b/Shader Decompiler/Program.cs index 063c794..65bfef4 100644 --- a/Shader Decompiler/Program.cs +++ b/Shader Decompiler/Program.cs @@ -146,6 +146,7 @@ static void Main(string[] args) DumpStages = args.Contains("--dump-stages"), DumpKeywords = args.Contains("--dump-keywords"), SaveSubprograms = args.Contains("--save-subprograms"), + RestoreKeywordVariants = args.Contains("--restore-keyword-variants"), FuseTemps = !args.Contains("--no-fuse-temps"), ReconstructSurfaceShaders = !args.Contains("--no-surface-shaders"), KeepPasses = args.Contains("--keep-passes"), @@ -189,7 +190,8 @@ private static int RunFaithfulnessTest(string folder, string[] args) result = Decompiler.Decompile(project, new DecompileOptions { OutRoot = outRoot, - FuseTemps = !args.Contains("--no-fuse-temps"), + RestoreKeywordVariants = args.Contains("--restore-keyword-variants"), + FuseTemps = !args.Contains("--no-fuse-temps"), ReconstructSurfaceShaders = !args.Contains("--no-surface-shaders"), KeepPasses = args.Contains("--keep-passes"), Verbose = false, diff --git a/Shader Decompiler/RuntimeVariantManifest.cs b/Shader Decompiler/RuntimeVariantManifest.cs new file mode 100644 index 0000000..86e958e --- /dev/null +++ b/Shader Decompiler/RuntimeVariantManifest.cs @@ -0,0 +1,89 @@ +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Text.RegularExpressions; +using Parser.Core.Hlsl.Ast; + +namespace Parser; + +internal sealed record RuntimeCompilationCase(int Pass, int Variant, string Hlsl, + int Vertex, int Fragment, string VertexProfile, string FragmentProfile); + +internal static class RuntimeVariantManifest +{ + internal const string Marker = "#error No serialized program matches this shader keyword state"; + + internal static void Write(string shaderPath, string text, HlslShaderNode shader, + IReadOnlyList programs) + { + Dictionary byIndex = programs.ToDictionary(program => program.Index); + object manifest = new + { + schemaVersion = 1, + shaderSha256 = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(text))), + passes = shader.SubShaders.SelectMany(sub => sub.Passes).Select((pass, index) => new + { + passIndex = index, + variants = pass.RuntimeVariants!.States.Select(state => new + { + vertex = state.Vertex, fragment = state.Fragment, keywords = state.Keywords, + vertexProfile = byIndex[state.Vertex].Dxbc.Shader!.ShaderModel, + fragmentProfile = byIndex[state.Fragment].Dxbc.Shader!.ShaderModel, + }), + }), + }; + File.WriteAllText(Path.ChangeExtension(shaderPath, ".variants.json"), + JsonSerializer.Serialize(manifest, new JsonSerializerOptions { WriteIndented = true })); + } + + internal static List Read(string shaderPath) + { + string text = File.ReadAllText(shaderPath); + using JsonDocument manifest = JsonDocument.Parse(File.ReadAllText(Path.ChangeExtension(shaderPath, ".variants.json"))); + JsonElement root = manifest.RootElement; + string hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(text))); + if (root.GetProperty("schemaVersion").GetInt32() != 1 + || !string.Equals(root.GetProperty("shaderSha256").GetString(), hash, StringComparison.OrdinalIgnoreCase)) + throw new InvalidDataException("Variant manifest does not match the generated shader."); + MatchCollection blocks = Regex.Matches(text, @"HLSLPROGRAM(.*?)ENDHLSL", RegexOptions.Singleline); + JsonElement passes = root.GetProperty("passes"); + if (passes.GetArrayLength() != blocks.Count || blocks.Count == 0) + throw new InvalidDataException("Variant manifest has an invalid pass count."); + List cases = new List(); + int passIndex = 0; + foreach (JsonElement pass in passes.EnumerateArray()) + { + if (pass.GetProperty("passIndex").GetInt32() != passIndex) + throw new InvalidDataException("Variant manifest pass order is invalid."); + int variantIndex = 0; + foreach (JsonElement variant in pass.GetProperty("variants").EnumerateArray()) + { + string[] keywords = variant.GetProperty("keywords").EnumerateArray().Select(value => value.GetString()!).ToArray(); + if (keywords.Any(keyword => !Regex.IsMatch(keyword, @"\A[A-Za-z_][A-Za-z0-9_]*\z"))) + throw new InvalidDataException("Variant manifest contains an invalid keyword."); + string vertexProfile = variant.GetProperty("vertexProfile").GetString()!; + string fragmentProfile = variant.GetProperty("fragmentProfile").GetString()!; + if (!Regex.IsMatch(vertexProfile, @"\Avs_[45]_0\z") || !Regex.IsMatch(fragmentProfile, @"\Aps_[45]_0\z")) + throw new InvalidDataException("Variant manifest contains an unsupported shader profile."); + string definitions = string.Concat(keywords.Select(keyword => $"#define {keyword} 1\n")); + cases.Add(new RuntimeCompilationCase(passIndex, variantIndex++, definitions + blocks[passIndex].Groups[1].Value, + variant.GetProperty("vertex").GetInt32(), variant.GetProperty("fragment").GetInt32(), vertexProfile, fragmentProfile)); + } + if (variantIndex == 0) throw new InvalidDataException("Variant manifest contains an empty pass."); + passIndex++; + } + return cases; + } + + internal static void VerifyCompilation(string shaderPath) + { + List cases = Read(shaderPath); + foreach (RuntimeCompilationCase item in cases) + { + RecompileVerify.CompileHlsl(item.Hlsl, "vert", item.VertexProfile); + RecompileVerify.CompileHlsl(item.Hlsl, "frag", item.FragmentProfile); + Console.WriteLine($"pass{item.Pass + 1} variant{item.Variant + 1}: compiled programs {item.Vertex}/{item.Fragment}"); + } + Console.WriteLine($"passes={cases.Select(item => item.Pass).Distinct().Count()} variants={cases.Count} compiled={cases.Count} compileFailures=0"); + } +} diff --git a/Shader Decompiler/RuntimeVariantPlan.cs b/Shader Decompiler/RuntimeVariantPlan.cs new file mode 100644 index 0000000..7ea436e --- /dev/null +++ b/Shader Decompiler/RuntimeVariantPlan.cs @@ -0,0 +1,84 @@ +using System.Text.RegularExpressions; +using Parser.DXBC.Metadata; + +namespace Parser; + +internal sealed record RuntimeVariantState(int Vertex, int Fragment, string[] Keywords); +internal sealed record RuntimeKeywordGroup(string[] Keywords, bool AllowsDisabled, bool Local); + +internal sealed class RuntimeVariantPlan +{ + internal required string[] Mask { get; init; } + internal required List States { get; init; } + internal required List Groups { get; init; } + internal required string[] StrippedKeywords { get; init; } + internal required HashSet LocalKeywords { get; init; } + + internal static RuntimeVariantPlan Build(IReadOnlyList programs, + IReadOnlyDictionary keywordFlags) + { + VariantPairing.Pair(programs); + string[] mask = programs.SelectMany(p => p.KeywordMask).Distinct(StringComparer.Ordinal) + .Order(StringComparer.Ordinal).ToArray(); + foreach (string keyword in mask) + { + if (!Regex.IsMatch(keyword, @"\A[A-Za-z_][A-Za-z0-9_]*\z")) + throw new InvalidDataException($"Invalid shader keyword '{keyword}'."); + if (!keywordFlags.TryGetValue(keyword, out int flags) || flags is not (0 or 1)) + throw new InvalidDataException($"Missing or unsupported serialized scope flags for keyword '{keyword}'."); + } + + Dictionary states = new Dictionary(StringComparer.Ordinal); + foreach (ProgramVariantMetadata vertex in programs.Where(p => p.Stage == "Vertex")) + foreach (ProgramVariantMetadata fragment in programs.Where(p => p.Stage == "Fragment")) + { + HashSet shared = vertex.KeywordMask.Intersect(fragment.KeywordMask).ToHashSet(StringComparer.Ordinal); + if (!vertex.Keywords.Where(shared.Contains).ToHashSet(StringComparer.Ordinal) + .SetEquals(fragment.Keywords.Where(shared.Contains))) continue; + string[] enabled = vertex.Keywords.Union(fragment.Keywords, StringComparer.Ordinal) + .Order(StringComparer.Ordinal).ToArray(); + string key = string.Join(";", enabled); + RuntimeVariantState state = new RuntimeVariantState(vertex.BlobIndex, fragment.BlobIndex, enabled); + if (states.TryGetValue(key, out RuntimeVariantState? previous) + && (previous.Vertex != state.Vertex || previous.Fragment != state.Fragment)) + throw new InvalidDataException($"Ambiguous shader programs for keyword state '{key}'."); + states[key] = state; + } + + HashSet[] enabledStates = states.Values.Select(s => s.Keywords.ToHashSet(StringComparer.Ordinal)).ToArray(); + string[] active = enabledStates.SelectMany(s => s).Distinct(StringComparer.Ordinal).Order(StringComparer.Ordinal).ToArray(); + HashSet remaining = active.ToHashSet(StringComparer.Ordinal); + List groups = new List(); + long combinations = 1; + while (remaining.Count != 0) + { + HashSet group = new HashSet(StringComparer.Ordinal) { remaining.Order(StringComparer.Ordinal).First() }; + Queue pending = new Queue(group); + while (pending.TryDequeue(out string? keyword)) + foreach (string other in active) + if (!enabledStates.Any(state => state.Contains(keyword) && state.Contains(other)) && group.Add(other)) + pending.Enqueue(other); + if (enabledStates.Any(state => state.Count(group.Contains) > 1)) + throw new InvalidDataException("Serialized keyword states cannot be represented by independent Unity keyword sets."); + bool allowsDisabled = enabledStates.Any(state => !state.Overlaps(group)); + combinations = checked(combinations * (group.Count + (allowsDisabled ? 1 : 0))); + if (combinations > states.Count) + throw new InvalidDataException("Serialized variants are incomplete for the reconstructed Unity keyword sets."); + if (group.Select(keyword => keywordFlags[keyword]).Distinct().Count() != 1) + throw new InvalidDataException("An exclusive keyword set mixes local and global scope."); + groups.Add(new RuntimeKeywordGroup(group.Order(StringComparer.Ordinal).ToArray(), allowsDisabled, keywordFlags[group.First()] == 1)); + remaining.ExceptWith(group); + } + // Disjoint exclusive sets plus equal cardinality prove the exact Cartesian product. + if (combinations != states.Count) + throw new InvalidDataException("Serialized variants do not form complete Unity keyword sets."); + return new RuntimeVariantPlan + { + Mask = mask, + States = states.OrderBy(p => p.Key, StringComparer.Ordinal).Select(p => p.Value).ToList(), + Groups = groups, + StrippedKeywords = mask.Except(active, StringComparer.Ordinal).ToArray(), + LocalKeywords = mask.Where(keyword => keywordFlags[keyword] == 1).ToHashSet(StringComparer.Ordinal), + }; + } +} diff --git a/Shader Decompiler/Tests/RecompileVerify.cs b/Shader Decompiler/Tests/RecompileVerify.cs index bd101f0..3107284 100644 --- a/Shader Decompiler/Tests/RecompileVerify.cs +++ b/Shader Decompiler/Tests/RecompileVerify.cs @@ -26,6 +26,11 @@ public static class RecompileVerify public static void Run(string shaderFile) { string text = File.ReadAllText(shaderFile); + if (text.Contains(RuntimeVariantManifest.Marker, StringComparison.Ordinal)) + { + RuntimeVariantManifest.VerifyCompilation(shaderFile); + return; + } string dir = Path.GetDirectoryName(shaderFile) ?? "."; // The .shader is written to the Output ROOT while its blob.bin lives // in the per-name subfolder (Output\{name}\blob.bin); the folder @@ -457,4 +462,4 @@ private static string ReadComBlobString(IntPtr blob) [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate nuint GetBufferSizeFn(IntPtr self); -} \ No newline at end of file +} diff --git a/Shader Decompiler/VariantPairing.cs b/Shader Decompiler/VariantPairing.cs index c0004e4..424bb82 100644 --- a/Shader Decompiler/VariantPairing.cs +++ b/Shader Decompiler/VariantPairing.cs @@ -1,17 +1,41 @@ using Parser.DXBC.Metadata; +using Parser.DXBC.Chunks; namespace Parser; internal static class VariantPairing { + internal static bool InterfacesMatch(IReadOnlyList outputs, IReadOnlyList inputs) + { + // Adjacent DirectX stages share register locations as well as semantics. + foreach (SignatureElement input in inputs.Where(element => element.SystemValue == 0)) + { + int available = outputs.Where(output => + string.Equals(output.SemanticName, input.SemanticName, StringComparison.OrdinalIgnoreCase) + && output.SemanticIndex == input.SemanticIndex && output.ComponentType == input.ComponentType + && output.Register == input.Register) + .Aggregate(0, (mask, output) => mask | output.Mask); + if ((available & input.Mask) != input.Mask) return false; + } + return true; + } + internal static List<(int Vertex, int Fragment)> Pair(IReadOnlyList programs) { + if (programs.Any(p => p.BlobIndex < 0 || p.Keywords.Any(string.IsNullOrWhiteSpace) + || p.KeywordMask.Any(string.IsNullOrWhiteSpace) + || p.Keywords.Distinct(StringComparer.Ordinal).Count() != p.Keywords.Length + || p.KeywordMask.Distinct(StringComparer.Ordinal).Count() != p.KeywordMask.Length)) + throw new InvalidDataException("Shader variants contain invalid indices or duplicate/empty keywords."); if (programs.Any(p => p.Stage is not ("Vertex" or "Fragment"))) throw new InvalidDataException("Serialized variant pairing currently supports vertex/fragment stages only."); ProgramVariantMetadata[] vertices = programs.Where(p => p.Stage == "Vertex").ToArray(); ProgramVariantMetadata[] fragments = programs.Where(p => p.Stage == "Fragment").ToArray(); if (programs.Any(p => p.Keywords.Except(p.KeywordMask).Any())) throw new InvalidDataException("A shader variant enables a keyword outside its stage mask."); + foreach (ProgramVariantMetadata[] stage in new[] { vertices, fragments }) + if (stage.Any(p => !p.KeywordMask.ToHashSet(StringComparer.Ordinal).SetEquals(stage[0].KeywordMask))) + throw new InvalidDataException("Shader variants have inconsistent keyword masks within a stage."); List<(int Vertex, int Fragment)> pairs = new List<(int Vertex, int Fragment)>(); foreach (ProgramVariantMetadata vertex in vertices) foreach (ProgramVariantMetadata fragment in fragments) diff --git a/libs/metadata.py b/libs/metadata.py index 2a8f8d1..e1f5a16 100644 --- a/libs/metadata.py +++ b/libs/metadata.py @@ -8,6 +8,7 @@ """ import json +import base64 class ShaderMetadata: @@ -27,6 +28,7 @@ def build(self): "fallback": self._get(parsed, "m_FallbackName", ""), "customEditor": self._get(parsed, "m_CustomEditorName", ""), "keywords": self._get(parsed, "m_KeywordNames", []), + "keywordFlags": self._keyword_flags(parsed), "dependencies": self._get(parsed, "m_Dependencies", []), "disableNoSubshadersMessage": self._get( parsed, "m_DisableNoSubshadersMessage", False @@ -161,6 +163,20 @@ def _pass(self, p, name_layouts=None, keyword_names=()): "keywords": p.get("m_NameIndices", []), } + @staticmethod + def _keyword_flags(parsed): + flags = parsed.get("m_KeywordFlags") + if flags is None: + return {} + if isinstance(flags, str): + flags = list(base64.b64decode(flags, validate=True)) + names = parsed.get("m_KeywordNames", []) + if (not isinstance(flags, list) or len(flags) != len(names) + or len(set(names)) != len(names) + or any(type(flag) is not int or not 0 <= flag <= 255 for flag in flags)): + raise ValueError("Invalid serialized shader keyword flags") + return dict(zip(names, flags)) + @staticmethod def _directx_programs(shader_pass, keyword_names): result = [] diff --git a/tests/UnityRuntime/README.md b/tests/UnityRuntime/README.md new file mode 100644 index 0000000..6af81a4 --- /dev/null +++ b/tests/UnityRuntime/README.md @@ -0,0 +1,22 @@ +# Runtime keyword regression + +The native fixture decompiles six synthetic DXBC programs into two ShaderLab +passes. The Unity check reads nine GPU results, including material keyword +changes and global overrides of global versus local keywords. No game assets +are needed. Use Windows, Direct3D 11 and Unity **6000.0.75f1** (Built-in pipeline). + +With `UNITY_CLI` pointing to the Unity CLI executable and `TEST_PROJECT` to a +disposable project created with that Editor version: + +```powershell +$env:SHADER_RUNTIME_TEST_OUTPUT = "$env:TEST_PROJECT/Assets" +dotnet test UnityShaderDecompiler.sln -c Release --filter FullyQualifiedName~RuntimeVariantEmissionTests +New-Item -ItemType Directory -Force "$env:TEST_PROJECT/Assets/Editor" | Out-Null +Copy-Item tests/UnityRuntime/RuntimeKeywordChecks.cs "$env:TEST_PROJECT/Assets/Editor/" +& $env:UNITY_CLI run $env:TEST_PROJECT --editor-version 6000.0.75f1 --timeout 180 --format json -- -executeMethod RuntimeKeywordChecks.Run -force-d3d11 +``` + +Require a successful CLI exit and `runtime-keyword-results.json` with +`passed: true` and `cases: 9` in the project directory. These checks validate +keyword selection, pass order and scope; they do not prove arbitrary shader +arithmetic or resource bindings equivalent to an original application. diff --git a/tests/UnityRuntime/RuntimeKeywordChecks.cs b/tests/UnityRuntime/RuntimeKeywordChecks.cs new file mode 100644 index 0000000..503d9d7 --- /dev/null +++ b/tests/UnityRuntime/RuntimeKeywordChecks.cs @@ -0,0 +1,91 @@ +using System; +using System.IO; +using UnityEditor; +using UnityEngine; +using UnityEngine.Rendering; + +// Run in a disposable Built-in project after exporting the native xUnit fixture into Assets. +public static class RuntimeKeywordChecks +{ + public static void Run() + { + if (Application.unityVersion != "6000.0.75f1") + throw new InvalidOperationException("Runtime shader checks target Unity 6000.0.75f1."); + if (SystemInfo.graphicsDeviceType != GraphicsDeviceType.Direct3D11) + throw new InvalidOperationException("Runtime shader checks require Direct3D 11."); + AssetDatabase.ImportAsset("Assets/RuntimeKeywords.shader", ImportAssetOptions.ForceSynchronousImport); + Shader shader = AssetDatabase.LoadAssetAtPath("Assets/RuntimeKeywords.shader"); + if (shader == null || ShaderUtil.ShaderHasError(shader)) + throw new InvalidOperationException("Generated shader failed Unity import."); + LocalKeyword mode = new LocalKeyword(shader, "MODE"); + LocalKeyword detail = new LocalKeyword(shader, "DETAIL"); + if (!mode.isValid || !mode.isOverridable || !detail.isValid || detail.isOverridable) + throw new InvalidOperationException("Keyword scopes differ from the serialized metadata."); + Material material = new Material(shader); + RenderTexture target = new RenderTexture(4, 4, 0, RenderTextureFormat.ARGBFloat, RenderTextureReadWrite.Linear); + Texture2D readback = new Texture2D(4, 4, TextureFormat.RGBAFloat, false, true); + Mesh quad = new Mesh + { + vertices = new[] { new Vector3(-1f, -1f, 0f), new Vector3(-1f, 1f, 0f), + new Vector3(1f, 1f, 0f), new Vector3(1f, -1f, 0f) }, + triangles = new[] { 0, 1, 2, 0, 2, 3 }, + }; + RenderTexture previous = RenderTexture.active; + bool previousSrgb = GL.sRGBWrite; + int cases = 0; + try + { + if (material.passCount != 2 || material.FindPass("Pass0") != 0 || material.FindPass("Pass1") != 1) + throw new InvalidOperationException("Serialized pass count or order changed."); + Shader.DisableKeyword("MODE"); + Shader.DisableKeyword("DETAIL"); + for (int pass = 0; pass < 2; pass++) + for (int state = 0; state < 4; state++) + { + material.SetKeyword(mode, (state & 1) != 0); + material.SetKeyword(detail, (state & 2) != 0); + Check(material, pass, target, readback, quad, new Color( + (state & 1) != 0 ? 0.75f : 0.25f, + (state & 2) != 0 ? 0.75f : 0.25f, + pass == 0 ? 0.5f : 1f, 1f)); + cases++; + } + material.SetKeyword(mode, false); + material.SetKeyword(detail, false); + Shader.EnableKeyword("MODE"); + Shader.EnableKeyword("DETAIL"); + Check(material, 0, target, readback, quad, new Color(0.75f, 0.25f, 0.5f, 1f)); + cases++; + if (ShaderUtil.ShaderHasError(shader)) + throw new InvalidOperationException("Shader compilation failed while selecting a variant."); + File.WriteAllText("runtime-keyword-results.json", "{\"passed\":true,\"cases\":" + cases + ",\"backend\":\"Direct3D11\"}"); + Debug.Log($"Runtime keyword checks passed: {cases} GPU readbacks."); + } + finally + { + Shader.DisableKeyword("MODE"); + Shader.DisableKeyword("DETAIL"); + RenderTexture.active = previous; + GL.sRGBWrite = previousSrgb; + UnityEngine.Object.DestroyImmediate(material); + UnityEngine.Object.DestroyImmediate(readback); + UnityEngine.Object.DestroyImmediate(target); + UnityEngine.Object.DestroyImmediate(quad); + } + } + + private static void Check(Material material, int pass, RenderTexture target, Texture2D readback, Mesh quad, Color expected) + { + RenderTexture.active = target; + GL.sRGBWrite = false; + GL.Clear(true, true, Color.magenta); + if (!material.SetPass(pass)) throw new InvalidOperationException("Failed to activate the generated pass."); + Graphics.DrawMeshNow(quad, Matrix4x4.identity); + readback.ReadPixels(new Rect(0, 0, 4, 4), 0, 0); + readback.Apply(); + Color actual = readback.GetPixel(2, 2); + if (Mathf.Abs(actual.r - expected.r) > 0.001f || Mathf.Abs(actual.g - expected.g) > 0.001f + || Mathf.Abs(actual.b - expected.b) > 0.001f || Mathf.Abs(actual.a - expected.a) > 0.001f) + throw new InvalidOperationException($"Pass {pass}: expected {expected}, read {actual} from the GPU."); + } +} diff --git a/tests/UnityShaderDecompiler.Tests/RuntimeVariantEmissionTests.cs b/tests/UnityShaderDecompiler.Tests/RuntimeVariantEmissionTests.cs new file mode 100644 index 0000000..2ad9b56 --- /dev/null +++ b/tests/UnityShaderDecompiler.Tests/RuntimeVariantEmissionTests.cs @@ -0,0 +1,115 @@ +using System.Text; +using System.Text.Json; +using System.Text.RegularExpressions; +using AssetStudio; +using Parser; + +namespace UnityShaderDecompiler.Tests; + +public sealed class RuntimeVariantEmissionTests +{ + [Fact] + [Trait("Category", "D3D")] + public void NativeProgramsKeepTheirPassesAndSelectByKeywords() + { + if (!OperatingSystem.IsWindows()) return; + using TempDirectory folder = new TempDirectory(); + List<(ShaderGpuProgramType Stage, byte[] Code)> programs = new List<(ShaderGpuProgramType, byte[])>(); + const string varying = "struct Varyings { float4 position:SV_POSITION; float4 marker:TEXCOORD0; }; "; + foreach (string value in new[] { "0.25", "0.75" }) + programs.Add((ShaderGpuProgramType.DX11VertexSM40, RecompileVerify.CompileHlsl( + varying + $"Varyings main(float4 p:POSITION) {{ Varyings o; o.marker=float4({value},0,0,1); o.position=p; return o; }}", "main", "vs_4_0"))); + foreach (string blue in new[] { "0.5", "1.0" }) + foreach (string green in new[] { "0.25", "0.75" }) + programs.Add((ShaderGpuProgramType.DX11PixelSM40, RecompileVerify.CompileHlsl( + varying + $"float4 main(Varyings i):SV_Target {{ return float4(i.marker.x,{green},{blue},1); }}", "main", "ps_4_0"))); + File.WriteAllBytes(Path.Combine(folder.Path, "blob.bin"), BuildBlob(programs)); + List passes = new List(); + for (int pass = 0; pass < 2; pass++) + passes.Add(new + { + name = "Pass" + pass, + renderState = new { cull = 0, zTest = 8, zWrite = false }, + directXPrograms = new object[] + { + new { blobIndex = 0, stage = "Vertex", keywords = Array.Empty(), keywordMask = new[] { "MODE" } }, + new { blobIndex = 1, stage = "Vertex", keywords = new[] { "MODE" }, keywordMask = new[] { "MODE" } }, + new { blobIndex = 2 + 2 * pass, stage = "Fragment", keywords = Array.Empty(), keywordMask = new[] { "DETAIL" } }, + new { blobIndex = 3 + 2 * pass, stage = "Fragment", keywords = new[] { "DETAIL" }, keywordMask = new[] { "DETAIL" } }, + }, + }); + string metadata = JsonSerializer.Serialize(new + { + name = "Test/RuntimeKeywords", keywordFlags = new { MODE = 0, DETAIL = 1 }, + subShaders = new[] { new { passes } }, + }); + File.WriteAllText(Path.Combine(folder.Path, "metadata.json"), metadata); + DecompilationResult result = Decompiler.DecompileFolder(folder.Path, new DecompileOptions + { + RestoreKeywordVariants = true, ReconstructSurfaceShaders = false, FuseTemps = false, Verbose = false, + OutRoot = Path.Combine(folder.Path, "generated"), + }); + Assert.Equal(2, result.Passes.Count); + Assert.Equal(2, Regex.Matches(result.ShaderText, @"\bHLSLPROGRAM\b").Count); + Assert.All(result.Passes, pass => Assert.Equal(4, pass.VariantPrograms.Count)); + Assert.Contains("#pragma multi_compile_local __ DETAIL", result.ShaderText); + Assert.Contains("#pragma multi_compile __ MODE", result.ShaderText); + Assert.Contains("#error No serialized program matches", result.ShaderText); + string shaderPath = Path.Combine(folder.Path, "generated", "Test_RuntimeKeywords.shader"); + List cases = RuntimeVariantManifest.Read(shaderPath); + Assert.Equal(8, cases.Count); + Assert.All(cases, item => Assert.Equal("vs_4_0", item.VertexProfile)); + RuntimeVariantManifest.VerifyCompilation(shaderPath); + File.AppendAllText(shaderPath, "\n// Changed shader\n"); + Assert.Throws(() => RuntimeVariantManifest.Read(shaderPath)); + + foreach (Match match in Regex.Matches(result.ShaderText, @"HLSLPROGRAM(.*?)ENDHLSL", RegexOptions.Singleline)) + for (int state = 0; state < 4; state++) + { + string definitions = ((state & 1) != 0 ? "#define MODE 1\n" : "") + + ((state & 2) != 0 ? "#define DETAIL 1\n" : ""); + Assert.NotEmpty(RecompileVerify.CompileHlsl(definitions + match.Groups[1].Value, "vert", "vs_4_0")); + Assert.NotEmpty(RecompileVerify.CompileHlsl(definitions + match.Groups[1].Value, "frag", "ps_4_0")); + } + + // Optional artifact export lets the Unity GPU test consume this exact generated shader. + string? output = Environment.GetEnvironmentVariable("SHADER_RUNTIME_TEST_OUTPUT"); + if (!string.IsNullOrEmpty(output)) + { + Directory.CreateDirectory(output); + File.WriteAllText(Path.Combine(output, "RuntimeKeywords.shader"), result.ShaderText); + } + } + + private static byte[] BuildBlob(List<(ShaderGpuProgramType Stage, byte[] Code)> programs) + { + List payloads = new List(); + foreach ((ShaderGpuProgramType stage, byte[] code) in programs) + { + using MemoryStream payload = new MemoryStream(); + using BinaryWriter writer = new BinaryWriter(payload, Encoding.UTF8, true); + writer.Write(202012090); + writer.Write((int)stage); + writer.Write(new byte[16]); + writer.Write(0); + writer.Write(code.Length + 1); + writer.Write((byte)0); // Raw Unity variant wrapper. + writer.Write(code); + while (payload.Length % 4 != 0) writer.Write((byte)0); + payloads.Add(payload.ToArray()); + } + using MemoryStream blob = new MemoryStream(); + using BinaryWriter table = new BinaryWriter(blob, Encoding.UTF8, true); + table.Write(payloads.Count); + int offset = 4 + 12 * payloads.Count; + foreach (byte[] payload in payloads) + { + table.Write(offset); + table.Write(payload.Length); + table.Write(0); + offset += payload.Length; + } + foreach (byte[] payload in payloads) table.Write(payload); + return blob.ToArray(); + } +} diff --git a/tests/UnityShaderDecompiler.Tests/RuntimeVariantPlanTests.cs b/tests/UnityShaderDecompiler.Tests/RuntimeVariantPlanTests.cs new file mode 100644 index 0000000..eeeb243 --- /dev/null +++ b/tests/UnityShaderDecompiler.Tests/RuntimeVariantPlanTests.cs @@ -0,0 +1,108 @@ +using Parser; +using Parser.DXBC.Metadata; + +namespace UnityShaderDecompiler.Tests; + +public sealed class RuntimeVariantPlanTests +{ + [Fact] + public void IndependentAndExclusiveKeywordsReconstructExactlyTheSerializedStates() + { + string[][] states = [ ["RED"], ["BLUE"], ["RED", "DETAIL"], ["BLUE", "DETAIL"] ]; + RuntimeVariantPlan plan = Plan(states, ["RED", "BLUE", "DETAIL", "STRIPPED"]); + Assert.Equal(4, plan.States.Count); + Assert.Contains(plan.Groups, group => group.Keywords.SequenceEqual(new[] { "BLUE", "RED" }) && !group.AllowsDisabled); + Assert.Contains(plan.Groups, group => group.Keywords.SequenceEqual(new[] { "DETAIL" }) && group.AllowsDisabled); + Assert.Equal(new[] { "STRIPPED" }, plan.StrippedKeywords); + } + + [Fact] + public void CorrelatedKeywordsAreRejectedInsteadOfInventingCombinations() + { + Assert.Throws(() => Plan([[], ["A", "B"]], ["A", "B"])); + } + + [Fact] + public void IncompleteExclusiveSetsAreRejected() + { + Assert.Throws(() => Plan([[], ["A"], ["B"], ["A", "C"]], ["A", "B", "C"])); + } + + [Fact] + public void DuplicateStateWithDifferentProgramsIsAmbiguous() + { + ProgramVariantMetadata[] programs = [new(0, "Vertex", [], []), new(1, "Vertex", [], []), new(2, "Fragment", [], [])]; + Assert.Throws(() => RuntimeVariantPlan.Build(programs, new Dictionary())); + } + + [Fact] + public void OneProgramCanServeMultipleKeywordStates() + { + ProgramVariantMetadata[] programs = [new(0, "Vertex", [], ["MODE"]), new(0, "Vertex", ["MODE"], ["MODE"]), + new(1, "Fragment", [], ["MODE"]), new(1, "Fragment", ["MODE"], ["MODE"])]; + RuntimeVariantPlan plan = RuntimeVariantPlan.Build(programs, new Dictionary { ["MODE"] = 0 }); + Assert.Equal(2, plan.States.Count); + } + + [Theory] + [InlineData("BAD NAME")] + [InlineData("NAME\n#error injected")] + [InlineData("1NAME")] + public void KeywordsMustBePreprocessorIdentifiers(string keyword) + { + Assert.Throws(() => Plan([[], [keyword]], [keyword])); + } + + [Fact] + public void MissingScopeMetadataIsNotGuessed() + { + ProgramVariantMetadata[] programs = [new(0, "Vertex", [], ["MODE"]), new(1, "Fragment", [], ["MODE"])]; + Assert.Throws(() => RuntimeVariantPlan.Build(programs, new Dictionary())); + } + + [Fact] + public void MasksMustBeConsistentWithinEachStage() + { + ProgramVariantMetadata[] programs = [new(0, "Vertex", [], []), new(1, "Vertex", [], ["MODE"]), new(2, "Fragment", [], [])]; + Assert.Throws(() => VariantPairing.Pair(programs)); + } + + [Theory] + [InlineData(2)] + [InlineData(255)] + public void UnknownScopeFlagsAreRejected(int flags) + { + ProgramVariantMetadata[] programs = [new(0, "Vertex", [], ["MODE"]), new(1, "Fragment", [], [])]; + Assert.Throws(() => RuntimeVariantPlan.Build(programs, + new Dictionary { ["MODE"] = flags })); + } + + [Fact] + public void ExclusiveGroupsCannotMixLocalAndGlobalKeywords() + { + ProgramVariantMetadata[] programs = [new(0, "Vertex", ["A"], ["A", "B"]), + new(1, "Vertex", ["B"], ["A", "B"]), new(2, "Fragment", [], [])]; + Assert.Throws(() => RuntimeVariantPlan.Build(programs, + new Dictionary { ["A"] = 0, ["B"] = 1 })); + } + + [Fact] + public void DisabledLocalKeywordsRetainTheirScope() + { + ProgramVariantMetadata[] programs = [new(0, "Vertex", [], ["MODE"]), new(1, "Fragment", [], [])]; + RuntimeVariantPlan plan = RuntimeVariantPlan.Build(programs, new Dictionary { ["MODE"] = 1 }); + Assert.Contains("MODE", plan.StrippedKeywords); + Assert.Contains("MODE", plan.LocalKeywords); + } + + private static RuntimeVariantPlan Plan(string[][] states, string[] mask) + { + List programs = new List(); + for (int index = 0; index < states.Length; index++) + { + programs.Add(new ProgramVariantMetadata(index * 2, "Vertex", states[index], mask)); + programs.Add(new ProgramVariantMetadata(index * 2 + 1, "Fragment", states[index], mask)); + } + return RuntimeVariantPlan.Build(programs, mask.ToDictionary(keyword => keyword, _ => 0)); + } +} diff --git a/tests/UnityShaderDecompiler.Tests/VariantInterfaceTests.cs b/tests/UnityShaderDecompiler.Tests/VariantInterfaceTests.cs new file mode 100644 index 0000000..683f33e --- /dev/null +++ b/tests/UnityShaderDecompiler.Tests/VariantInterfaceTests.cs @@ -0,0 +1,25 @@ +using Parser; +using Parser.DXBC.Chunks; + +namespace UnityShaderDecompiler.Tests; + +public sealed class VariantInterfaceTests +{ + [Theory] + [InlineData(1, 3, 3, true)] + [InlineData(0, 3, 3, false)] + [InlineData(1, 1, 3, false)] + [InlineData(1, 3, 7, false)] + public void AdjacentStagesRequireMatchingRegisterTypeAndComponents(int register, int type, int mask, bool expected) + { + SignatureElement output = new SignatureElement + { + SemanticName = "TEXCOORD", Register = 1, ComponentType = 3, Mask = 3, + }; + SignatureElement input = new SignatureElement + { + SemanticName = "texcoord", Register = (uint)register, ComponentType = (uint)type, Mask = (byte)mask, + }; + Assert.Equal(expected, VariantPairing.InterfacesMatch([output], [input])); + } +} diff --git a/tests/test_keyword_flags.py b/tests/test_keyword_flags.py new file mode 100644 index 0000000..6813d6d --- /dev/null +++ b/tests/test_keyword_flags.py @@ -0,0 +1,24 @@ +import unittest + +from libs.metadata import ShaderMetadata + + +class KeywordFlagsTests(unittest.TestCase): + def test_array_and_base64_keep_local_and_global_scope(self): + for flags in ([0, 1], "AAE="): + with self.subTest(flags=flags): + self.assertEqual(ShaderMetadata._keyword_flags({ + "m_KeywordNames": ["GLOBAL", "LOCAL"], "m_KeywordFlags": flags, + }), {"GLOBAL": 0, "LOCAL": 1}) + + def test_missing_flags_are_not_invented(self): + self.assertEqual(ShaderMetadata._keyword_flags({"m_KeywordNames": ["MODE"]}), {}) + + def test_malformed_flags_fail(self): + for flags in ([], [0, 1], [True], [-1], [256], "!bad!"): + with self.subTest(flags=flags), self.assertRaises(ValueError): + ShaderMetadata._keyword_flags({"m_KeywordNames": ["MODE"], "m_KeywordFlags": flags}) + + def test_duplicate_names_fail(self): + with self.assertRaises(ValueError): + ShaderMetadata._keyword_flags({"m_KeywordNames": ["MODE", "MODE"], "m_KeywordFlags": [0, 1]}) From c3c12826a5beaf532532a6bed7230586257f8afd Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 13:38:21 +0100 Subject: [PATCH 44/93] Preserve serialized integer shader property defaults --- README.md | 4 ++ Shader Decompiler/Core/Hlsl/HlslAST.cs | 5 ++- .../Core/Hlsl/HlslPrettyPrinter.cs | 12 ++++++ .../IntegerPropertyTests.cs | 40 +++++++++++++++++++ 4 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 tests/UnityShaderDecompiler.Tests/IntegerPropertyTests.cs diff --git a/README.md b/README.md index b646a84..695bf63 100644 --- a/README.md +++ b/README.md @@ -314,6 +314,10 @@ root, prefixing each with ## Limitations +Serialized integer properties (type 5) emit as ShaderLab `Integer`, preserving +the integer bits stored in the float default slot. Non-finite JSON defaults are +rejected because their original integer payload cannot be recovered reliably. + - Targets Unity 2022.3 D3D11 shader blobs. - Vertex and fragment stages are fully reconstructed. Hull, geometry, domain, and compute programs are parsed and IR-built, but the AST/emit path mainly diff --git a/Shader Decompiler/Core/Hlsl/HlslAST.cs b/Shader Decompiler/Core/Hlsl/HlslAST.cs index 93478c0..de8dd91 100644 --- a/Shader Decompiler/Core/Hlsl/HlslAST.cs +++ b/Shader Decompiler/Core/Hlsl/HlslAST.cs @@ -30,7 +30,7 @@ public sealed class HlslShaderNode } // Mirrors Unity's serialized shader property type enum -// (m_Type: Color=0, Vector=1, Float=2, Range=3, Texture=4). +// (m_Type: Color=0, Vector=1, Float=2, Range=3, Texture=4, Int=5). public enum HlslPropertyKind { Color = 0, @@ -38,6 +38,7 @@ public enum HlslPropertyKind Float = 2, Range = 3, Texture = 4, + Integer = 5, Unknown = -1, } @@ -322,4 +323,4 @@ public sealed class HlslFieldNode // float vs int vs uint) — left as a hint for Stage 8 to firm up, not // invented here. public string TypeHint { get; init; } = "float4"; -} \ No newline at end of file +} diff --git a/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs b/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs index 222df18..12b90ba 100644 --- a/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs +++ b/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs @@ -87,6 +87,7 @@ private static void PrintProperties(StringBuilder sb, List pro HlslPropertyKind.Color => "Color", HlslPropertyKind.Vector => "Vector", HlslPropertyKind.Float => "Float", + HlslPropertyKind.Integer => "Integer", HlslPropertyKind.Range when p.Range is { } r => $"Range({r.Min}, {r.Max})", // Range with no recovered bounds (metadata carried no default value, // so min/max slots are absent) — emit as Float rather than @@ -108,6 +109,17 @@ private static string PropertyDefaultText(HlslPropertyNode p) if (p.Kind == HlslPropertyKind.Texture) return "\"\" {}"; + if (p.Kind == HlslPropertyKind.Integer) + { + // Unity stores integer property bits in the serialized float slot. + float value = p.DefaultComponents.Length == 4 ? p.DefaultComponents[0] + : p.DefaultValue is { } integerDefault && integerDefault.TryGetProperty("x", out JsonElement x) + ? x.GetSingle() : throw new InvalidDataException("Integer property has no serialized default."); + if (!float.IsFinite(value)) + throw new InvalidDataException("Integer property bits cannot be recovered from a non-finite JSON float."); + return BitConverter.SingleToInt32Bits(value).ToString(System.Globalization.CultureInfo.InvariantCulture); + } + // Typed default (Metadata layer, float[4] — the same {x,y,z,w} the // printer used to read out of the raw JsonElement below). if (p.DefaultComponents.Length == 4) diff --git a/tests/UnityShaderDecompiler.Tests/IntegerPropertyTests.cs b/tests/UnityShaderDecompiler.Tests/IntegerPropertyTests.cs new file mode 100644 index 0000000..ab64909 --- /dev/null +++ b/tests/UnityShaderDecompiler.Tests/IntegerPropertyTests.cs @@ -0,0 +1,40 @@ +using System.Text.Json; +using Parser.Core.Hlsl.Ast; +using Parser.DXBC.Metadata; + +namespace UnityShaderDecompiler.Tests; + +public sealed class IntegerPropertyTests +{ + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(42)] + [InlineData(1065353216)] + [InlineData(int.MinValue)] + public void SerializedIntegerDefaultsPreserveBits(int expected) + { + float serialized = BitConverter.Int32BitsToSingle(expected); + ShaderMetadata metadata = new ShaderMetadata + { + Name = "Test/IntegerProperty", + }; + metadata.Properties.Add(new ShaderProperty + { + Name = "_Count", Type = "5", DefaultComponents = [serialized, 0, 0, 0], + }); + string text = HlslPrettyPrinter.Print(HlslAstBuilder.BuildShell(metadata)); + Assert.Contains($"_Count (\"_Count\", Integer) = {expected}", text); + } + + [Fact] + public void NonFiniteDefaultCannotRecoverAnIntegerPayload() + { + HlslShaderNode shader = new HlslShaderNode(); + shader.Properties.Add(new HlslPropertyNode + { + Kind = HlslPropertyKind.Integer, DefaultComponents = [float.NaN, 0, 0, 0], + }); + Assert.Throws(() => HlslPrettyPrinter.Print(shader)); + } +} From de23300086e4a227237f75e30d370ea4e54f60bd Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 13:40:35 +0100 Subject: [PATCH 45/93] Reject missing integer payloads during metadata loading --- .../DXBC/Metadata/Shadermetadata.cs | 8 +++++++- .../IntegerPropertyTests.cs | 16 +++++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/Shader Decompiler/DXBC/Metadata/Shadermetadata.cs b/Shader Decompiler/DXBC/Metadata/Shadermetadata.cs index 63bc48b..e667f5a 100644 --- a/Shader Decompiler/DXBC/Metadata/Shadermetadata.cs +++ b/Shader Decompiler/DXBC/Metadata/Shadermetadata.cs @@ -121,6 +121,12 @@ public sealed class ShaderProperty private static float[] ParseDefaultComponents(JsonElement el) { + if (el.TryGetProperty("type", out JsonElement propertyType) && propertyType.ToString() == "5" + && (!el.TryGetProperty("defaultValue", out JsonElement integerDefault) + || integerDefault.ValueKind != JsonValueKind.Object + || !integerDefault.TryGetProperty("x", out JsonElement integerBits) + || integerBits.ValueKind != JsonValueKind.Number)) + throw new InvalidDataException("Integer property requires a numeric serialized default."); if (!el.TryGetProperty("defaultValue", out var dv) || dv.ValueKind != JsonValueKind.Object) return Array.Empty(); @@ -271,4 +277,4 @@ public sealed class CbufferVariableMetadata : 0, IsMatrix = el.TryGetProperty("isMatrix", out var m) && m.ValueKind == JsonValueKind.True, }; -} \ No newline at end of file +} diff --git a/tests/UnityShaderDecompiler.Tests/IntegerPropertyTests.cs b/tests/UnityShaderDecompiler.Tests/IntegerPropertyTests.cs index ab64909..fe7ee82 100644 --- a/tests/UnityShaderDecompiler.Tests/IntegerPropertyTests.cs +++ b/tests/UnityShaderDecompiler.Tests/IntegerPropertyTests.cs @@ -19,10 +19,11 @@ public void SerializedIntegerDefaultsPreserveBits(int expected) { Name = "Test/IntegerProperty", }; - metadata.Properties.Add(new ShaderProperty + using JsonDocument json = JsonDocument.Parse(JsonSerializer.Serialize(new { - Name = "_Count", Type = "5", DefaultComponents = [serialized, 0, 0, 0], - }); + name = "_Count", type = 5, defaultValue = new { x = serialized, y = 0, z = 0, w = 0 }, + })); + metadata.Properties.Add(ShaderProperty.From(json.RootElement)); string text = HlslPrettyPrinter.Print(HlslAstBuilder.BuildShell(metadata)); Assert.Contains($"_Count (\"_Count\", Integer) = {expected}", text); } @@ -37,4 +38,13 @@ public void NonFiniteDefaultCannotRecoverAnIntegerPayload() }); Assert.Throws(() => HlslPrettyPrinter.Print(shader)); } + + [Theory] + [InlineData("{\"type\":5}")] + [InlineData("{\"type\":5,\"defaultValue\":{\"x\":\"NaN\"}}")] + public void MissingOrUnrecoverableIntegerDefaultsFail(string input) + { + using JsonDocument json = JsonDocument.Parse(input); + Assert.Throws(() => ShaderProperty.From(json.RootElement)); + } } From 3004d5b38cf00928fe6ed51ded68527b718066c6 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 13:40:36 +0100 Subject: [PATCH 46/93] Check every serialized state and expose Unity variant compilation checks --- Shader Decompiler/Program.cs | 2 +- Shader Decompiler/VariantPairing.cs | 8 +- tests/UnityRuntime/README.md | 8 ++ tests/UnityRuntime/ShaderVariantChecks.cs | 80 +++++++++++++++++++ .../RuntimeVariantPlanTests.cs | 9 +++ 5 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 tests/UnityRuntime/ShaderVariantChecks.cs diff --git a/Shader Decompiler/Program.cs b/Shader Decompiler/Program.cs index 65bfef4..f234bb6 100644 --- a/Shader Decompiler/Program.cs +++ b/Shader Decompiler/Program.cs @@ -191,7 +191,7 @@ private static int RunFaithfulnessTest(string folder, string[] args) { OutRoot = outRoot, RestoreKeywordVariants = args.Contains("--restore-keyword-variants"), - FuseTemps = !args.Contains("--no-fuse-temps"), + FuseTemps = !args.Contains("--no-fuse-temps"), ReconstructSurfaceShaders = !args.Contains("--no-surface-shaders"), KeepPasses = args.Contains("--keep-passes"), Verbose = false, diff --git a/Shader Decompiler/VariantPairing.cs b/Shader Decompiler/VariantPairing.cs index 424bb82..364e584 100644 --- a/Shader Decompiler/VariantPairing.cs +++ b/Shader Decompiler/VariantPairing.cs @@ -37,16 +37,20 @@ internal static bool InterfacesMatch(IReadOnlyList outputs, IR if (stage.Any(p => !p.KeywordMask.ToHashSet(StringComparer.Ordinal).SetEquals(stage[0].KeywordMask))) throw new InvalidDataException("Shader variants have inconsistent keyword masks within a stage."); List<(int Vertex, int Fragment)> pairs = new List<(int Vertex, int Fragment)>(); + HashSet matched = new HashSet(); foreach (ProgramVariantMetadata vertex in vertices) foreach (ProgramVariantMetadata fragment in fragments) { HashSet shared = vertex.KeywordMask.Intersect(fragment.KeywordMask).ToHashSet(); if (vertex.Keywords.Where(shared.Contains).ToHashSet().SetEquals(fragment.Keywords.Where(shared.Contains))) + { pairs.Add((vertex.BlobIndex, fragment.BlobIndex)); + matched.Add(vertex); + matched.Add(fragment); + } } // A fragment-only keyword can reuse a vertex program; pairing is not 1:1. - if (vertices.Any(v => !pairs.Any(p => p.Vertex == v.BlobIndex)) || - fragments.Any(f => !pairs.Any(p => p.Fragment == f.BlobIndex)) || pairs.Count == 0) + if (programs.Any(program => !matched.Contains(program)) || pairs.Count == 0) throw new InvalidDataException("Serialized shader variants contain an unmatched stage."); return pairs.Distinct().ToList(); } diff --git a/tests/UnityRuntime/README.md b/tests/UnityRuntime/README.md index 6af81a4..53e9956 100644 --- a/tests/UnityRuntime/README.md +++ b/tests/UnityRuntime/README.md @@ -20,3 +20,11 @@ Require a successful CLI exit and `runtime-keyword-results.json` with `passed: true` and `cases: 9` in the project directory. These checks validate keyword selection, pass order and scope; they do not prove arbitrary shader arithmetic or resource bindings equivalent to an original application. + +To compile every combination of other generated shaders, copy each `.shader` +and matching `.variants.json` under a project asset folder, copy +`ShaderVariantChecks.cs` into `Assets/Editor`, and set +`SHADER_VARIANT_DIRECTORY` to that folder (for example `Assets/Generated`). +Run the same CLI command with `-executeMethod ShaderVariantChecks.Run`. +It checks the shader hash, pass count and nonempty compiled bytecode for both +stages of every manifest entry. Results go to `shader-variant-results.json`. diff --git a/tests/UnityRuntime/ShaderVariantChecks.cs b/tests/UnityRuntime/ShaderVariantChecks.cs new file mode 100644 index 0000000..949e883 --- /dev/null +++ b/tests/UnityRuntime/ShaderVariantChecks.cs @@ -0,0 +1,80 @@ +using System; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using UnityEditor; +using UnityEditor.Rendering; +using UnityEngine; +using UnityEngine.Rendering; + +public static class ShaderVariantChecks +{ + [Serializable] private sealed class Manifest + { + public int schemaVersion; + public string shaderSha256; + public Pass[] passes; + } + [Serializable] private sealed class Pass + { + public int passIndex; + public Variant[] variants; + } + [Serializable] private sealed class Variant { public string[] keywords; } + + public static void Run() + { + if (Application.unityVersion != "6000.0.75f1" || SystemInfo.graphicsDeviceType != GraphicsDeviceType.Direct3D11) + throw new InvalidOperationException("Variant checks require Unity 6000.0.75f1 and Direct3D 11."); + string folder = Environment.GetEnvironmentVariable("SHADER_VARIANT_DIRECTORY"); + if (string.IsNullOrEmpty(folder) || !folder.Replace('\\', '/').StartsWith("Assets/", StringComparison.Ordinal)) + throw new InvalidOperationException("SHADER_VARIANT_DIRECTORY must name a folder under Assets/."); + string[] paths = Directory.GetFiles(folder, "*.shader", SearchOption.AllDirectories); + if (paths.Length == 0) throw new InvalidOperationException("No generated shaders found."); + int compiled = 0; + foreach (string path in paths.OrderBy(value => value, StringComparer.Ordinal)) + { + Manifest manifest = JsonUtility.FromJson(File.ReadAllText(Path.ChangeExtension(path, ".variants.json"))); + using (SHA256 hasher = SHA256.Create()) + { + string hash = BitConverter.ToString(hasher.ComputeHash(File.ReadAllBytes(path))).Replace("-", ""); + if (manifest.schemaVersion != 1 || !string.Equals(hash, manifest.shaderSha256, StringComparison.OrdinalIgnoreCase)) + throw new InvalidOperationException("Manifest does not match shader: " + path); + } + string assetPath = path.Replace('\\', '/'); + AssetDatabase.ImportAsset(assetPath, ImportAssetOptions.ForceSynchronousImport); + Shader shader = AssetDatabase.LoadAssetAtPath(assetPath); + if (shader == null || ShaderUtil.ShaderHasError(shader)) + throw new InvalidOperationException("Shader import failed: " + path); + ShaderData data = ShaderUtil.GetShaderData(shader); + int flatPass = 0; + for (int sub = 0; sub < data.SubshaderCount; sub++) + { + ShaderData.Subshader subshader = data.GetSubshader(sub); + for (int pass = 0; pass < subshader.PassCount; pass++, flatPass++) + { + if (flatPass >= manifest.passes.Length || manifest.passes[flatPass].passIndex != flatPass) + throw new InvalidOperationException("Imported pass order differs from manifest."); + Pass entry = manifest.passes[flatPass]; + if (entry.variants == null || entry.variants.Length == 0) + throw new InvalidOperationException("Pass has no variants."); + foreach (Variant variant in entry.variants) + foreach (ShaderType stage in new[] { ShaderType.Vertex, ShaderType.Fragment }) + { + ShaderData.VariantCompileInfo result = subshader.GetPass(pass).CompileVariant(stage, + variant.keywords, ShaderCompilerPlatform.D3D, BuildTarget.StandaloneWindows64); + if (!result.Success || result.ShaderData == null || result.ShaderData.Length == 0) + throw new InvalidOperationException($"{path} pass {flatPass} {stage} [{string.Join(",", variant.keywords)}]: " + + string.Join("; ", result.Messages.Select(message => message.message))); + compiled++; + } + } + } + if (flatPass != manifest.passes.Length || ShaderUtil.ShaderHasError(shader)) + throw new InvalidOperationException("Imported pass count or compilation differs from manifest."); + Debug.Log($"Validated {path}: {flatPass} passes."); + } + File.WriteAllText("shader-variant-results.json", "{\"passed\":true,\"shaders\":" + paths.Length + + ",\"compiledStages\":" + compiled + ",\"editor\":\"" + Application.unityVersion + "\"}"); + } +} diff --git a/tests/UnityShaderDecompiler.Tests/RuntimeVariantPlanTests.cs b/tests/UnityShaderDecompiler.Tests/RuntimeVariantPlanTests.cs index eeeb243..943bb24 100644 --- a/tests/UnityShaderDecompiler.Tests/RuntimeVariantPlanTests.cs +++ b/tests/UnityShaderDecompiler.Tests/RuntimeVariantPlanTests.cs @@ -44,6 +44,15 @@ public void OneProgramCanServeMultipleKeywordStates() Assert.Equal(2, plan.States.Count); } + [Fact] + public void ReusedBlobDoesNotHideAnUnmatchedKeywordState() + { + ProgramVariantMetadata[] programs = [new(0, "Vertex", [], ["MODE"]), new(0, "Vertex", ["MODE"], ["MODE"]), + new(1, "Fragment", [], ["MODE"])]; + Assert.Throws(() => RuntimeVariantPlan.Build(programs, + new Dictionary { ["MODE"] = 0 })); + } + [Theory] [InlineData("BAD NAME")] [InlineData("NAME\n#error injected")] From 315031894da1f0040721fef9d9f75db1de73160d Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 13:55:05 +0100 Subject: [PATCH 47/93] Preserve saturated moves and low-bit integer conditions --- README.md | 4 +++ .../Core/Hlsl/HlslPrettyPrinter.cs | 11 +++++++ .../DXBC/IR/Builders/IRBuilderArithmetic.cs | 9 +++--- docs/pr-structured-buffer-loads.md | 6 ++-- .../RawRegisterSemanticsTests.cs | 2 +- .../RegisterRoundTrip.cs | 5 +++ .../SaturatedMoveTests.cs | 32 +++++++++++++++++++ 7 files changed, 62 insertions(+), 7 deletions(-) create mode 100644 tests/UnityShaderDecompiler.Tests/SaturatedMoveTests.cs diff --git a/README.md b/README.md index e2a8ac6..8baa4f9 100644 --- a/README.md +++ b/README.md @@ -315,6 +315,10 @@ root, prefixing each with ## Limitations +With `--no-fuse-temps`, saturated moves clamp floating-point values before +storing raw bits. Integer reads remain in integer storage, preserving low-bit +conditions that fxc can otherwise fold away through a float round trip. + - Targets Unity 2022.3 D3D11 shader blobs. - Structured-buffer loads require `--no-fuse-temps`. Element stride, constant byte offsets and swizzles are preserved; fused mode, dynamic offsets and diff --git a/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs b/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs index fc95afb..e8dfa7c 100644 --- a/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs +++ b/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs @@ -2638,6 +2638,17 @@ private static string RenderIntrinsic(IRExpression.IntrinsicExpression expr, Pri string Arg(int n) => RenderExpression(expr.Arguments[n], ctx); string AllArgs() => string.Join(", ", expr.Arguments.Select(a => RenderExpression(a, ctx))); + // Keep integer reads in integer storage: fxc can fold a known subnormal + // float round-trip to zero even when it only transports register bits. + if (ctx.RawTemporaryStorage && expr.Intrinsic is IRExpression.IRIntrinsic.AsUInt or IRExpression.IRIntrinsic.AsInt + && expr.Arguments[0] is IRExpression.RegisterExpression { Register: { } register } + && register.RegisterType is RegisterType.Temp or RegisterType.IndexableTemp + && register.Modifier == ShdrParser.OperandModifier.None) + { + string bits = RenderTempRead(register, ctx); + return expr.Intrinsic == IRExpression.IRIntrinsic.AsUInt ? bits : $"asint({bits})"; + } + // fxc folds frac(x + integer) even with /Gis, losing the add's rounding. // Use the DXBC definition of frc to retain that dependency. if (ctx.RawTemporaryStorage && expr.Intrinsic == IRExpression.IRIntrinsic.FractionalPart) diff --git a/Shader Decompiler/DXBC/IR/Builders/IRBuilderArithmetic.cs b/Shader Decompiler/DXBC/IR/Builders/IRBuilderArithmetic.cs index 3810f93..4e32bdf 100644 --- a/Shader Decompiler/DXBC/IR/Builders/IRBuilderArithmetic.cs +++ b/Shader Decompiler/DXBC/IR/Builders/IRBuilderArithmetic.cs @@ -8,7 +8,8 @@ public partial class IRBuilder private void BuildMov(IRProgram program, Instruction instruction) { - Emit(program, instruction, _preserveRegisterBits + // Saturated moves interpret the payload as float before clamping. + Emit(program, instruction, _preserveRegisterBits && !instruction.Saturate ? BuildRawExpression(instruction.Operands[1]) : BuildExpression(instruction.Operands[1])); } @@ -18,8 +19,8 @@ private void BuildMovC(IRProgram program, Instruction instruction) new IRExpression.ConditionalExpression { Condition = BuildBoolExpression(instruction.Operands[1]), - TrueExpression = _preserveRegisterBits ? BuildRawExpression(instruction.Operands[2]) : BuildExpression(instruction.Operands[2]), - FalseExpression = _preserveRegisterBits ? BuildRawExpression(instruction.Operands[3]) : BuildExpression(instruction.Operands[3]) + TrueExpression = _preserveRegisterBits && !instruction.Saturate ? BuildRawExpression(instruction.Operands[2]) : BuildExpression(instruction.Operands[2]), + FalseExpression = _preserveRegisterBits && !instruction.Saturate ? BuildRawExpression(instruction.Operands[3]) : BuildExpression(instruction.Operands[3]) }; Emit(program, instruction, expression); @@ -523,4 +524,4 @@ private void BuildDMax(IRProgram program, Instruction instruction) => private void BuildDMin(IRProgram program, Instruction instruction) => BuildTypedBinaryIntrinsic(program, instruction, IRExpression.IRIntrinsic.Min, BuildDoubleExpression); -} \ No newline at end of file +} diff --git a/docs/pr-structured-buffer-loads.md b/docs/pr-structured-buffer-loads.md index 14e2ba7..69e9753 100644 --- a/docs/pr-structured-buffer-loads.md +++ b/docs/pr-structured-buffer-loads.md @@ -4,6 +4,8 @@ Float temporary storage can corrupt integer moves and structured-buffer results. Structured loads preserve element stride, byte offset and resource swizzle independently. These changes belong together because correct addressing alone still loses integer output bits without raw register transport. Structured loads require `--no-fuse-temps`; fused mode, dynamic offsets and unaligned or out-of-range accesses fail explicitly. This PR includes the former #7; no separate dependency remains. -Native round trips cover signed/unsigned `mov` and `movc`, plus float/uint/int structured loads with stride, offset and swizzle. A restricted bytecode evaluator checks exact output words, including NaN payloads, subnormals and signed zero, and rejects unexpected operations. Regression checks also verify rejection of fused structured loads. These are compiler/bytecode checks, not GPU execution tests. +Saturated moves clamp the floating-point value before storing its bits. Integer reads bypass float round trips that fxc can fold to zero for known subnormals; this preserves low-bit branch conditions. -Validation: 65 C# tests and all four PicaVoxel end-to-end cases pass on Windows. The isolated suite uses en-US because upstream's fr-FR golden-output failure is addressed separately in #5. The README documents both raw register preservation and the structured-load restriction. +Native round trips cover signed/unsigned moves, saturation and low-bit selection, plus float/uint/int structured loads with stride, offset and swizzle. A restricted bytecode evaluator checks exact output words, including NaN payloads, subnormals and signed zero, and rejects unexpected operations. The saturation regression fails with the former implementation and passes with the fix. These are compiler/bytecode checks, not GPU execution tests. + +Validation: 67 C# tests and all four PicaVoxel end-to-end cases pass on Windows. The isolated suite uses en-US because upstream's fr-FR golden-output failure is addressed separately in #5. The README documents both raw register preservation and the structured-load restriction. diff --git a/tests/UnityShaderDecompiler.Tests/RawRegisterSemanticsTests.cs b/tests/UnityShaderDecompiler.Tests/RawRegisterSemanticsTests.cs index 02658b9..bdfabd5 100644 --- a/tests/UnityShaderDecompiler.Tests/RawRegisterSemanticsTests.cs +++ b/tests/UnityShaderDecompiler.Tests/RawRegisterSemanticsTests.cs @@ -184,7 +184,7 @@ public void RawTemporaryStorageKeepsComparisonMasksAndIntegerArithmetic() string source = Print(function); Assert.Contains("uint r0_x_1", source); Assert.Contains("? 0xffffffffu : 0u", source); - Assert.Contains("asint(asfloat(r0_x_1))", source); + Assert.Contains("asint(r0_x_1)", source); Assert.Contains("uint r1_x_1 = asuint(", source); } diff --git a/tests/UnityShaderDecompiler.Tests/RegisterRoundTrip.cs b/tests/UnityShaderDecompiler.Tests/RegisterRoundTrip.cs index 5d24407..24eb9a3 100644 --- a/tests/UnityShaderDecompiler.Tests/RegisterRoundTrip.cs +++ b/tests/UnityShaderDecompiler.Tests/RegisterRoundTrip.cs @@ -89,6 +89,11 @@ internal static uint[] Evaluate(DxbcFile file, uint[] input, uint[]? structuredW _ => throw new InvalidOperationException(), }; } + if (instruction.Saturate) + { + float value = BitConverter.UInt32BitsToSingle(result[lane]); + result[lane] = BitConverter.SingleToUInt32Bits(float.IsNaN(value) ? 0f : Math.Clamp(value, 0f, 1f)); + } } (RegisterType, uint) key = (destination.RegisterType, destination.RegisterIndex); if (!registers.TryGetValue(key, out uint[]? target)) registers[key] = target = new uint[4]; diff --git a/tests/UnityShaderDecompiler.Tests/SaturatedMoveTests.cs b/tests/UnityShaderDecompiler.Tests/SaturatedMoveTests.cs new file mode 100644 index 0000000..114c691 --- /dev/null +++ b/tests/UnityShaderDecompiler.Tests/SaturatedMoveTests.cs @@ -0,0 +1,32 @@ +using Parser.DXBC; +using Parser.DXBC.Instructions; + +namespace UnityShaderDecompiler.Tests; + +public sealed class SaturatedMoveTests +{ + [Theory] + [InlineData(false)] + [InlineData(true)] + [Trait("Category", "D3D")] + public void SaturatedMovesClampFloatValuesBeforeStoringBits(bool conditional) + { + if (!OperatingSystem.IsWindows()) return; + string selection = conditional ? "((asuint(value.y) & 1u) != 0u ? value : float4(-0.5, 0.25, 1.5, 0))" : "value"; + DxbcFile original = RegisterRoundTrip.Compile( + $"uint4 main(float4 value:TEXCOORD0):SV_Target {{ return asuint(saturate({selection})); }}"); + Assert.Contains(original.Shader!.Instructions, instruction => instruction.Saturate); + DxbcFile recompiled = RegisterRoundTrip.Recompile(original); + uint[] input = [0xbf000000u, 0x3e800001u, 0x3fc00000u, 0x7fc12345u]; + uint[] expected = [0, 0x3e800001u, 0x3f800000u, 0]; + Assert.Equal(expected, RegisterRoundTrip.Evaluate(original, input)); + Assert.Equal(expected, RegisterRoundTrip.Evaluate(recompiled, input)); + if (conditional) + { + input[1] = 0; + expected[1] = 0x3e800000u; + Assert.Equal(expected, RegisterRoundTrip.Evaluate(original, input)); + Assert.Equal(expected, RegisterRoundTrip.Evaluate(recompiled, input)); + } + } +} From 44f350b53b6dfe4359d513f05123a0ee0688d7e9 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 13:56:30 +0100 Subject: [PATCH 48/93] Document integer property compatibility and regression coverage --- docs/pr-integer-shader-properties.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docs/pr-integer-shader-properties.md diff --git a/docs/pr-integer-shader-properties.md b/docs/pr-integer-shader-properties.md new file mode 100644 index 0000000..614134b --- /dev/null +++ b/docs/pr-integer-shader-properties.md @@ -0,0 +1,5 @@ +# Preserve serialized integer shader properties + +Property type 5 stores an integer payload in a serialized float slot. Emit ShaderLab `Integer` and recover the original bits instead of treating that slot as a numeric float. Missing or non-finite JSON payloads fail explicitly because they cannot recover the original value reliably. + +Validation: 59 C# tests and all four PicaVoxel end-to-end cases pass, including positive values, negative zero bits, large integers and invalid payloads. The isolated suite uses en-US because upstream's fr-FR golden-output failure is addressed separately in #5. No game-specific fixtures or paths are included. From 91b79753c43775ba706be7d5e1fc1fc0d02d89e5 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 13:57:46 +0100 Subject: [PATCH 49/93] Document runtime validation and invalidate stale test reports --- docs/pr-serialized-variant-pairing.md | 8 ++++---- tests/UnityRuntime/RuntimeKeywordChecks.cs | 1 + tests/UnityRuntime/ShaderVariantChecks.cs | 2 ++ 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/pr-serialized-variant-pairing.md b/docs/pr-serialized-variant-pairing.md index d4c30b6..18f95d3 100644 --- a/docs/pr-serialized-variant-pairing.md +++ b/docs/pr-serialized-variant-pairing.md @@ -1,7 +1,7 @@ -# Pair shader variants using serialized program references +# Restore runtime keyword selection from serialized variants -Grouping programs by blob order can pair unrelated variants. Use serialized program indices and pair stage variants when their enabled keywords agree on the intersection of their keyword masks. Stage-only keywords remain independent: a vertex variant can therefore pair with multiple fragment-only variants. Reject missing programs and incompatible interfaces. +Use serialized program references and stage keyword masks to pair vertex/fragment programs. Preserve stage-only keywords and reject missing programs, unmatched states and incompatible interfaces. Keep constant-buffer sizes in extracted metadata. -Preserve constant-buffer sizes in the extracted metadata, including the existing zero sentinel for a binding with no serialized extent. A Python serialization test covers both paths. Runtime keyword selection remains unimplemented: compatible combinations are still emitted as separate passes. +`--restore-keyword-variants` retains the original passes and emits keyword-controlled program branches, including local/global scope. Reconstruct only exact, complete combinations of independent or exclusive keyword sets; missing scope flags, ambiguous mappings and unsupported correlations fail explicitly. The default expanded-pass mode remains available. A hash-bound manifest lets native compilation check every recovered combination. -Validation: 54 C# tests, two Python tests and all four PicaVoxel end-to-end cases pass. The isolated C# suite uses en-US; upstream's fr-FR golden-output failure is addressed separately in #5. Regenerate shared golden references after combining emitter changes. +Validation: 74 C# tests, six Python tests and all four PicaVoxel cases pass in isolation. Unity CLI with Editor 6000.0.75f1 and D3D11 passes nine GPU readbacks covering pass order, material keyword changes and global/local scope. Generic Unity checks also compile every manifest state. These tests do not establish full application rendering or resource-binding equivalence. Isolated C# tests use en-US; the existing fr-FR golden issue is addressed separately in #5. diff --git a/tests/UnityRuntime/RuntimeKeywordChecks.cs b/tests/UnityRuntime/RuntimeKeywordChecks.cs index 503d9d7..5117424 100644 --- a/tests/UnityRuntime/RuntimeKeywordChecks.cs +++ b/tests/UnityRuntime/RuntimeKeywordChecks.cs @@ -9,6 +9,7 @@ public static class RuntimeKeywordChecks { public static void Run() { + File.WriteAllText("runtime-keyword-results.json", "{\"passed\":false}"); if (Application.unityVersion != "6000.0.75f1") throw new InvalidOperationException("Runtime shader checks target Unity 6000.0.75f1."); if (SystemInfo.graphicsDeviceType != GraphicsDeviceType.Direct3D11) diff --git a/tests/UnityRuntime/ShaderVariantChecks.cs b/tests/UnityRuntime/ShaderVariantChecks.cs index 949e883..3c3812c 100644 --- a/tests/UnityRuntime/ShaderVariantChecks.cs +++ b/tests/UnityRuntime/ShaderVariantChecks.cs @@ -24,6 +24,7 @@ [Serializable] private sealed class Pass public static void Run() { + File.WriteAllText("shader-variant-results.json", "{\"passed\":false}"); if (Application.unityVersion != "6000.0.75f1" || SystemInfo.graphicsDeviceType != GraphicsDeviceType.Direct3D11) throw new InvalidOperationException("Variant checks require Unity 6000.0.75f1 and Direct3D 11."); string folder = Environment.GetEnvironmentVariable("SHADER_VARIANT_DIRECTORY"); @@ -68,6 +69,7 @@ public static void Run() + string.Join("; ", result.Messages.Select(message => message.message))); compiled++; } + Debug.Log($"Compiled {path} pass {flatPass}: {entry.variants.Length} variants."); } } if (flatPass != manifest.passes.Length || ShaderUtil.ShaderHasError(shader)) From 9117e52c4a0c09e0e9e5cd632a8c7e5923840f64 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 14:47:08 +0100 Subject: [PATCH 50/93] Make DirectX failure propagation opt-in with --fail-on-error --- README.md | 5 +++-- Shader Decompiler/Decompiler.cs | 5 ++++- Shader Decompiler/Program.cs | 9 ++++++++- docs/pr-directx-fail-fast.md | 4 ++-- .../DirectXFailureTests.cs | 18 ++++++++++++++---- 5 files changed, 31 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 4a4d0af..3d93201 100644 --- a/README.md +++ b/README.md @@ -120,8 +120,8 @@ dotnet run --project "Shader Decompiler/Shader Decompiler.csproj" -- Output/HOLO The result is written to `Shader Decompiler/Output/HOLO_Holo.shader`. -A DirectX subprogram failure aborts decompilation with its index and cause, -instead of silently omitting it from the final shader. +`--fail-on-error` aborts on a DirectX subprogram failure with its index and cause. +It is off by default, preserving the existing log-and-continue behavior. ## Usage @@ -178,6 +178,7 @@ and spaces are stripped). | `--out-root ` | Write `.shader` output and subprogram artifacts to `` instead of the project-local `Output/` | | `--dump-stages` | Print a full IR snapshot after every pipeline phase and every optimization pass | | `--dump-keywords` | Print each subprogram's global and local keyword sets | +| `--fail-on-error` | Abort on DirectX subprogram errors instead of logging and continuing (default: off) | | `--save-subprograms` | Also write per-subprogram `program{i}.bin`, `program{i}.dxbc`, and `program{i}.hlsl` (disassembly) | | `--no-fuse-temps` | Disable temp fusion; keep every SSA copy as a separate statement | | `--no-surface-shaders` | Disable surface-shader recognition; emit raw reconstructed HLSLPROGRAM passes | diff --git a/Shader Decompiler/Decompiler.cs b/Shader Decompiler/Decompiler.cs index 47df062..4fd2701 100644 --- a/Shader Decompiler/Decompiler.cs +++ b/Shader Decompiler/Decompiler.cs @@ -20,6 +20,9 @@ namespace Parser; /// public sealed class DecompileOptions { + /// Abort on DirectX subprogram errors instead of logging and continuing. + public bool FailOnError { get; init; } + /// Print the IR after every pipeline phase. public bool DumpStages { get; init; } @@ -341,7 +344,7 @@ public static DecompilationResult Decompile( } catch (Exception ex) { - if (sp.m_ProgramType is >= ShaderGpuProgramType.DX11VertexSM40 + if (options.FailOnError && sp.m_ProgramType is >= ShaderGpuProgramType.DX11VertexSM40 and <= ShaderGpuProgramType.DX11DomainSM50) { throw new InvalidDataException( diff --git a/Shader Decompiler/Program.cs b/Shader Decompiler/Program.cs index 063c794..1e5dba5 100644 --- a/Shader Decompiler/Program.cs +++ b/Shader Decompiler/Program.cs @@ -145,6 +145,7 @@ static void Main(string[] args) OutRoot = outRoot, DumpStages = args.Contains("--dump-stages"), DumpKeywords = args.Contains("--dump-keywords"), + FailOnError = args.Contains("--fail-on-error"), SaveSubprograms = args.Contains("--save-subprograms"), FuseTemps = !args.Contains("--no-fuse-temps"), ReconstructSurfaceShaders = !args.Contains("--no-surface-shaders"), @@ -153,7 +154,12 @@ static void Main(string[] args) Verbose = true, }; - Decompiler.Decompile(project, options); + try { Decompiler.Decompile(project, options); } + catch (Exception error) when (options.FailOnError) + { + Console.Error.WriteLine(error.Message); + Environment.ExitCode = 1; + } } // Locate the decompiler Output/ folder regardless of the launch directory. @@ -189,6 +195,7 @@ private static int RunFaithfulnessTest(string folder, string[] args) result = Decompiler.Decompile(project, new DecompileOptions { OutRoot = outRoot, + FailOnError = args.Contains("--fail-on-error"), FuseTemps = !args.Contains("--no-fuse-temps"), ReconstructSurfaceShaders = !args.Contains("--no-surface-shaders"), KeepPasses = args.Contains("--keep-passes"), diff --git a/docs/pr-directx-fail-fast.md b/docs/pr-directx-fail-fast.md index 96bdcf4..ffcca2c 100644 --- a/docs/pr-directx-fail-fast.md +++ b/docs/pr-directx-fail-fast.md @@ -1,5 +1,5 @@ # Propagate DirectX subprogram decompilation failures -A corrupt DirectX subprogram was logged and omitted while the export continued. Abort with the subprogram index and original exception, and leave no final shader. This production fix is absent from upstream `1850608`; its presence on a local master branch does not make this PR redundant. +A corrupt DirectX subprogram is logged and omitted by default. Opt in with `--fail-on-error` (API: `DecompileOptions.FailOnError = true`) to abort with the subprogram index and original exception, before writing the final shader. Existing commands and API calls retain their default behavior. -Validation: 53 C# tests and all four existing PicaVoxel extraction/decompilation tests pass. C# tests use an en-US default culture; the upstream fr-FR golden-output failure is addressed independently by the numeric-formatting PR. +Validation: 55 C# tests cover both failure policies; all four existing PicaVoxel extraction/decompilation tests pass. Isolated C# tests use en-US; the upstream fr-FR golden-output failure is addressed separately in #5. diff --git a/tests/UnityShaderDecompiler.Tests/DirectXFailureTests.cs b/tests/UnityShaderDecompiler.Tests/DirectXFailureTests.cs index f7fda00..4551883 100644 --- a/tests/UnityShaderDecompiler.Tests/DirectXFailureTests.cs +++ b/tests/UnityShaderDecompiler.Tests/DirectXFailureTests.cs @@ -7,9 +7,11 @@ namespace UnityShaderDecompiler.Tests; public class DirectXFailureTests { [Theory] - [InlineData(ShaderGpuProgramType.DX11VertexSM40)] - [InlineData(ShaderGpuProgramType.DX11PixelSM50)] - public void MalformedDirectXProgramStopsBeforeWritingTheShader(ShaderGpuProgramType stage) + [InlineData(ShaderGpuProgramType.DX11VertexSM40, true)] + [InlineData(ShaderGpuProgramType.DX11PixelSM50, true)] + [InlineData(ShaderGpuProgramType.DX11VertexSM40, false)] + [InlineData(ShaderGpuProgramType.DX11PixelSM50, false)] + public void MalformedDirectXProgramHonorsExplicitFailurePolicy(ShaderGpuProgramType stage, bool failOnError) { using TempDirectory folder = new(); using MemoryStream payload = new(); @@ -36,8 +38,16 @@ public void MalformedDirectXProgramStopsBeforeWritingTheShader(ShaderGpuProgramT "{\"name\":\"Test/InvalidDxbc\",\"subShaders\":[]}"); string output = Path.Combine(folder.Path, "generated"); + if (!failOnError) + { + Assert.False(new DecompileOptions().FailOnError); + Decompiler.DecompileFolder(folder.Path, new DecompileOptions { Verbose = false, OutRoot = output }); + Assert.Single(Directory.GetFiles(output, "*.shader")); + return; + } + InvalidDataException error = Assert.Throws(() => - Decompiler.DecompileFolder(folder.Path, new DecompileOptions { Verbose = false, OutRoot = output })); + Decompiler.DecompileFolder(folder.Path, new DecompileOptions { FailOnError = true, Verbose = false, OutRoot = output })); Assert.Contains("DirectX subprogram 0", error.Message); Assert.Contains(stage.ToString(), error.Message); From 777b5a8ef042d6e4bedb13356b1225669afc7323 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 14:47:08 +0100 Subject: [PATCH 51/93] Retain legacy SFI0 fields with explicit availability metadata --- README.md | 4 +- Shader Decompiler/DXBC/Chunks/SFI0 Chunk.cs | 41 +++++++++++++++++++ docs/pr-dxbc-feature-flags.md | 4 +- .../NativeFeatureInfoTests.cs | 23 +++++++++++ 4 files changed, 69 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1cb6258..f41a583 100644 --- a/README.md +++ b/README.md @@ -220,7 +220,9 @@ Each subprogram goes through `IRPipeline` (`DXBC/IR/IRPipeline.cs`): 2. **Container parse** — validate the `DXBC` magic and lengths; parse `ISGN`, `OSGN`, `OSG5`, `PSGN`, `PCON`, `SFI0`, `RDEF`, `STAT`, and the `SHDR`/`SHEX` shader chunk. `SFI0` is read as a 64-bit feature mask; invalid - lengths are rejected. Unknown chunks are preserved raw. + lengths are rejected. Legacy public statistic fields remain for compatibility; + `HasLegacyStatistics` is false because SFI0 does not contain those values. + Unknown chunks are preserved raw. 3. **Instruction decode** — decode every SM4/SM5 instruction and operand. 4. **IR build** — build flat, typed IR statements. 5. **CFG** — build basic blocks and edges. diff --git a/Shader Decompiler/DXBC/Chunks/SFI0 Chunk.cs b/Shader Decompiler/DXBC/Chunks/SFI0 Chunk.cs index a5cfb70..29fd9ca 100644 --- a/Shader Decompiler/DXBC/Chunks/SFI0 Chunk.cs +++ b/Shader Decompiler/DXBC/Chunks/SFI0 Chunk.cs @@ -5,6 +5,47 @@ public class Sfi0Chunk { public ulong FeatureFlags { get; private set; } + /// False: SFI0 has feature flags, not shader statistics. + public bool HasLegacyStatistics => false; + + // Retained for source/binary compatibility. These fields are never populated + // by SFI0; check HasLegacyStatistics before using them as statistics. + public uint InputPrimitive; + public uint OutputPrimitive; + public uint MaxInputSignatureRegisterCount; + public uint MaxOutputSignatureRegisterCount; + public uint MaxInputPatchConstantSignatureRegisterCount; + public uint MaxOutputPatchCount; + public uint PatchConstantPrimitive; + public uint CTemps; + public uint CIndexableTemp; + public uint CGsInstanceCount; + public uint CControlPoints; + public uint COutputControlPoints; + public uint CTgsm; + public uint CInputGsInstanceCount; + public uint COutputs; + public uint CInputs; + public uint CPatchConst; + public uint CNumThreadsX; + public uint CNumThreadsY; + public uint CNumThreadsZ; + public uint CGsOutputTopology; + public uint CInputPrimitive; + public uint CMaxOutputVertices; + public uint COutputTopology; + public uint CMaxTemp; + public uint CMaxOutputPatchCount; + public uint CMaxGsInstanceCount; + public uint CMaxIndexableTemp; + public uint CMaxInputGsInstanceCount; + public uint CMaxOutputs; + public uint CMaxInputs; + public uint CMaxPatchConst; + public uint CMaxNumThreadsX; + public uint CMaxNumThreadsY; + public uint CMaxNumThreadsZ; + public void Read(BinaryReader reader) { if (reader.BaseStream.Length - reader.BaseStream.Position != sizeof(ulong)) diff --git a/docs/pr-dxbc-feature-flags.md b/docs/pr-dxbc-feature-flags.md index c7aceac..4f5176d 100644 --- a/docs/pr-dxbc-feature-flags.md +++ b/docs/pr-dxbc-feature-flags.md @@ -1,5 +1,5 @@ # Read SFI0 as a 64-bit feature mask -`SFI0` is an eight-byte feature mask, not a statistics structure. Read its 64 bits and reject invalid chunk lengths. The corrected synthetic test and a native compiler fixture verify the actual format. +`SFI0` is an eight-byte feature mask, not a statistics structure. Read its 64 bits and reject invalid chunk lengths. Legacy public uint fields remain for source/binary compatibility, but are not populated by SFI0: `HasLegacyStatistics` explicitly returns false. Their default values must not be interpreted as recovered statistics; consumers should use `FeatureFlags` for this chunk. -Validation: 53 C# tests and all four existing PicaVoxel extraction/decompilation tests pass. C# tests use an en-US default culture; the upstream fr-FR golden-output failure is addressed independently by the numeric-formatting PR. +Validation: 54 C# tests, including the native feature mask and the 35 retained public fields, and all four PicaVoxel cases pass. Isolated C# tests use en-US; the upstream fr-FR golden-output failure is addressed separately in #5. diff --git a/tests/UnityShaderDecompiler.Tests/NativeFeatureInfoTests.cs b/tests/UnityShaderDecompiler.Tests/NativeFeatureInfoTests.cs index d05c1f3..93ecef5 100644 --- a/tests/UnityShaderDecompiler.Tests/NativeFeatureInfoTests.cs +++ b/tests/UnityShaderDecompiler.Tests/NativeFeatureInfoTests.cs @@ -7,6 +7,29 @@ namespace UnityShaderDecompiler.Tests; public class NativeFeatureInfoTests { + [Fact] + public void LegacyStatisticsFieldsRemainAvailableWithAnExplicitAvailabilityFlag() + { + using MemoryStream stream = new MemoryStream(BitConverter.GetBytes(1UL)); + using BinaryReader reader = new BinaryReader(stream); + Parser.DXBC.Chunks.Sfi0Chunk chunk = new Parser.DXBC.Chunks.Sfi0Chunk(); + chunk.Read(reader); + Assert.Equal(1UL, chunk.FeatureFlags); + Assert.False(chunk.HasLegacyStatistics); + string[] fields = + { + "InputPrimitive", "OutputPrimitive", "MaxInputSignatureRegisterCount", "MaxOutputSignatureRegisterCount", + "MaxInputPatchConstantSignatureRegisterCount", "MaxOutputPatchCount", "PatchConstantPrimitive", + "CTemps", "CIndexableTemp", "CGsInstanceCount", "CControlPoints", "COutputControlPoints", "CTgsm", + "CInputGsInstanceCount", "COutputs", "CInputs", "CPatchConst", "CNumThreadsX", "CNumThreadsY", "CNumThreadsZ", + "CGsOutputTopology", "CInputPrimitive", "CMaxOutputVertices", "COutputTopology", "CMaxTemp", "CMaxOutputPatchCount", + "CMaxGsInstanceCount", "CMaxIndexableTemp", "CMaxInputGsInstanceCount", "CMaxOutputs", "CMaxInputs", "CMaxPatchConst", + "CMaxNumThreadsX", "CMaxNumThreadsY", "CMaxNumThreadsZ", + }; + foreach (string field in fields) + Assert.Equal(typeof(uint), typeof(Parser.DXBC.Chunks.Sfi0Chunk).GetField(field)?.FieldType); + } + [Fact] [Trait("Category", "D3D")] public void FeatureFlagsMatchTheNativeCompilersEightBytePayload() From 77e8a39cd785093be1488244273ce652c92b30f5 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 14:47:09 +0100 Subject: [PATCH 52/93] Preserve default stage support and failure policy for serialized variants --- README.md | 3 +- Shader Decompiler/Decompiler.cs | 49 ++++++++------ Shader Decompiler/VariantPairing.cs | 41 ++++++++++++ docs/pr-serialized-variant-pairing.md | 4 +- .../RuntimeVariantEmissionTests.cs | 66 +++++++++++++++++++ .../VariantPairingTests.cs | 26 ++++++++ 6 files changed, 168 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 9695d48..f3290fd 100644 --- a/README.md +++ b/README.md @@ -239,7 +239,8 @@ Each subprogram goes through `IRPipeline` (`DXBC/IR/IRPipeline.cs`): Shader/SubShader/Pass shell comes from `metadata.json`. 12. **Pass and variant recovery** — use serialized program references when available, pairing stages whose enabled keywords agree within their shared - keyword masks. Validate stage interfaces; older metadata falls back to + keyword masks, including Geometry/Hull/Domain in expanded passes. Validate + stage interfaces; older metadata falls back to serialization order and signatures. Byte-identical pairs are deduplicated. 13. **Pretty print** — emit properties, tags, LOD, render state, `HLSLPROGRAM` blocks, structs, resources, and function bodies. diff --git a/Shader Decompiler/Decompiler.cs b/Shader Decompiler/Decompiler.cs index 8144ca6..4315133 100644 --- a/Shader Decompiler/Decompiler.cs +++ b/Shader Decompiler/Decompiler.cs @@ -160,6 +160,7 @@ public static DecompilationResult Decompile( .SelectMany(ss => ss.Passes) .ToList(); var candidates = new List(); + HashSet failedPrograms = new HashSet(); for (int i = 0; i < program.m_SubPrograms.Length; i++) { @@ -344,6 +345,7 @@ public static DecompilationResult Decompile( } catch (Exception ex) { + failedPrograms.Add(i); log.WriteLine($"No DXBC: {ex.Message}"); } @@ -358,7 +360,7 @@ public static DecompilationResult Decompile( if (options.RestoreKeywordVariants) BuildRuntimeVariantPasses(astShader, allPasses, candidates, project.Metadata.KeywordFlags, log); else - BuildVariantPasses(astShader, allPasses, candidates, log); + BuildVariantPasses(astShader, allPasses, candidates, log, failedPrograms); log.WriteLine(); log.WriteLine("HLSL AST (Stage 2)"); @@ -470,7 +472,7 @@ private static void BuildVariantPasses( HlslShaderNode astShader, List allPasses, List candidates, - TextWriter log) + TextWriter log, ISet failedPrograms) { if (candidates.Count == 0) return; @@ -488,7 +490,7 @@ private static void BuildVariantPasses( if (allPasses.Any(p => p.DirectXPrograms.Count != 0)) { - BuildSerializedVariantPasses(astShader, allPasses, candidates, textureNames, log); + BuildSerializedVariantPasses(astShader, allPasses, candidates, textureNames, log, failedPrograms); return; } @@ -605,35 +607,44 @@ private static void BuildVariantPasses( private static void BuildSerializedVariantPasses( HlslShaderNode shader, List shells, List candidates, - Dictionary textureNames, TextWriter log) + Dictionary textureNames, TextWriter log, ISet failedPrograms) { Dictionary byIndex = candidates.ToDictionary(c => c.Index); HashSet covered = new HashSet(); foreach (HlslPassNode shell in shells) { if (shell.DirectXPrograms.Count == 0) continue; - List<(int Vertex, int Fragment)> pairs = VariantPairing.Pair(shell.DirectXPrograms); + List groups = VariantPairing.Group(shell.DirectXPrograms); HlslSubShaderNode parent = shader.SubShaders.Single(ss => ss.Passes.Contains(shell)); int insertAt = parent.Passes.IndexOf(shell); - foreach ((int vertexIndex, int fragmentIndex) in pairs) + foreach (ProgramVariantMetadata[] group in groups) { - if (!byIndex.TryGetValue(vertexIndex, out DecompiledSubprogram? vertex) || - !byIndex.TryGetValue(fragmentIndex, out DecompiledSubprogram? fragment)) - throw new InvalidDataException("Serialized variant references a missing DXBC program."); - if (vertex.Stage != HlslShaderStage.Vertex || fragment.Stage != HlslShaderStage.Fragment) - throw new InvalidDataException("Serialized variant stage does not match its DXBC program."); - if (vertex.Dxbc.OutputSignature is null || fragment.Dxbc.InputSignature is null - || !VariantPairing.InterfacesMatch(vertex.Dxbc.OutputSignature.Elements, fragment.Dxbc.InputSignature.Elements)) - throw new InvalidDataException($"Variant {vertexIndex}/{fragmentIndex} has incompatible interpolators."); + if (group.Any(reference => failedPrograms.Contains(reference.BlobIndex))) + { + // Preserve the caller's existing log-and-continue failure policy. + covered.UnionWith(group.Select(reference => reference.BlobIndex)); + log.WriteLine("Skipped serialized variant after a logged subprogram error."); + continue; + } HlslPassNode pass = insertAt == parent.Passes.IndexOf(shell) ? shell : ClonePass(shell); if (!ReferenceEquals(pass, shell)) parent.Passes.Insert(insertAt, pass); - AttachPassProgram(pass, vertex, textureNames, log); - AttachPassProgram(pass, fragment, textureNames, log); - covered.Add(vertexIndex); - covered.Add(fragmentIndex); + DecompiledSubprogram? previous = null; + foreach (ProgramVariantMetadata reference in group) + { + if (!byIndex.TryGetValue(reference.BlobIndex, out DecompiledSubprogram? program)) + throw new InvalidDataException("Serialized variant references a missing DXBC program."); + if (program.Stage.ToString() != reference.Stage) + throw new InvalidDataException("Serialized variant stage does not match its DXBC program."); + if (previous is not null && (previous.Dxbc.OutputSignature is null || program.Dxbc.InputSignature is null + || !VariantPairing.InterfacesMatch(previous.Dxbc.OutputSignature.Elements, program.Dxbc.InputSignature.Elements))) + throw new InvalidDataException($"Variant {previous.Index}/{program.Index} has incompatible interpolators."); + AttachPassProgram(pass, program, textureNames, log); + covered.Add(program.Index); + previous = program; + } insertAt++; } - log.WriteLine($"Serialized variants: {pairs.Count} stage pairs."); + log.WriteLine($"Serialized variants: {groups.Count} stage combinations."); } if (byIndex.Keys.Except(covered).Any()) throw new InvalidDataException("DXBC programs are missing from serialized variant coverage."); diff --git a/Shader Decompiler/VariantPairing.cs b/Shader Decompiler/VariantPairing.cs index 364e584..060ac17 100644 --- a/Shader Decompiler/VariantPairing.cs +++ b/Shader Decompiler/VariantPairing.cs @@ -5,6 +5,47 @@ namespace Parser; internal static class VariantPairing { + // Expanded passes may contain any serialized graphics stage, not only vertex/fragment. + internal static List Group(IReadOnlyList programs) + { + string[] stageOrder = { "Vertex", "Hull", "Domain", "Geometry", "Fragment" }; + if (programs.Count == 0 || programs.Any(program => !stageOrder.Contains(program.Stage))) + throw new InvalidDataException("Serialized variants contain an unknown graphics stage."); + List groups = new List { Array.Empty() }; + foreach (string stageName in stageOrder) + { + ProgramVariantMetadata[] stage = programs.Where(program => program.Stage == stageName).ToArray(); + if (stage.Length == 0) continue; + foreach (ProgramVariantMetadata program in stage) + { + if (program.BlobIndex < 0 || program.Keywords.Any(string.IsNullOrWhiteSpace) + || program.KeywordMask.Any(string.IsNullOrWhiteSpace) + || program.Keywords.Distinct(StringComparer.Ordinal).Count() != program.Keywords.Length + || program.KeywordMask.Distinct(StringComparer.Ordinal).Count() != program.KeywordMask.Length + || program.Keywords.Except(program.KeywordMask).Any() + || !program.KeywordMask.ToHashSet(StringComparer.Ordinal).SetEquals(stage[0].KeywordMask)) + throw new InvalidDataException("Serialized variants contain invalid or inconsistent stage metadata."); + } + List next = new List(); + foreach (ProgramVariantMetadata[] group in groups) + foreach (ProgramVariantMetadata candidate in stage) + { + bool compatible = group.All(previous => + { + HashSet shared = previous.KeywordMask.Intersect(candidate.KeywordMask).ToHashSet(StringComparer.Ordinal); + return previous.Keywords.Where(shared.Contains).ToHashSet(StringComparer.Ordinal) + .SetEquals(candidate.Keywords.Where(shared.Contains)); + }); + if (compatible) next.Add(group.Append(candidate).ToArray()); + } + groups = next; + } + HashSet matched = groups.SelectMany(group => group).ToHashSet(); + if (programs.Any(program => !matched.Contains(program))) + throw new InvalidDataException("Serialized shader variants contain an unmatched stage."); + return groups.DistinctBy(group => string.Join(",", group.Select(program => program.BlobIndex))).ToList(); + } + internal static bool InterfacesMatch(IReadOnlyList outputs, IReadOnlyList inputs) { // Adjacent DirectX stages share register locations as well as semantics. diff --git a/docs/pr-serialized-variant-pairing.md b/docs/pr-serialized-variant-pairing.md index 18f95d3..7abee99 100644 --- a/docs/pr-serialized-variant-pairing.md +++ b/docs/pr-serialized-variant-pairing.md @@ -2,6 +2,8 @@ Use serialized program references and stage keyword masks to pair vertex/fragment programs. Preserve stage-only keywords and reject missing programs, unmatched states and incompatible interfaces. Keep constant-buffer sizes in extracted metadata. +Compatibility: default expanded passes also retain Geometry, Hull and Domain programs, matching their shared keyword states across the complete graphics pipeline. Default expanded mode also preserves log-and-continue after a recorded subprogram error; runtime reconstruction remains strict. Only the opt-in runtime reconstruction is restricted to vertex/fragment stages. A native geometry fixture reproduces the previous default-mode rejection and now passes; unit tests cover tessellation/geometry combinations and unmatched intermediate stages. + `--restore-keyword-variants` retains the original passes and emits keyword-controlled program branches, including local/global scope. Reconstruct only exact, complete combinations of independent or exclusive keyword sets; missing scope flags, ambiguous mappings and unsupported correlations fail explicitly. The default expanded-pass mode remains available. A hash-bound manifest lets native compilation check every recovered combination. -Validation: 74 C# tests, six Python tests and all four PicaVoxel cases pass in isolation. Unity CLI with Editor 6000.0.75f1 and D3D11 passes nine GPU readbacks covering pass order, material keyword changes and global/local scope. Generic Unity checks also compile every manifest state. These tests do not establish full application rendering or resource-binding equivalence. Isolated C# tests use en-US; the existing fr-FR golden issue is addressed separately in #5. +Validation: 78 C# tests, six Python tests and all four PicaVoxel cases pass in isolation. The prior runtime validation under Unity CLI with Editor 6000.0.75f1 and D3D11 passed nine GPU readbacks; the compatibility changes leave that opt-in path unchanged. These tests do not establish full application rendering or resource-binding equivalence. Isolated C# tests use en-US; the existing fr-FR golden issue is addressed separately in #5. diff --git a/tests/UnityShaderDecompiler.Tests/RuntimeVariantEmissionTests.cs b/tests/UnityShaderDecompiler.Tests/RuntimeVariantEmissionTests.cs index 2ad9b56..74c49d0 100644 --- a/tests/UnityShaderDecompiler.Tests/RuntimeVariantEmissionTests.cs +++ b/tests/UnityShaderDecompiler.Tests/RuntimeVariantEmissionTests.cs @@ -8,6 +8,72 @@ namespace UnityShaderDecompiler.Tests; public sealed class RuntimeVariantEmissionTests { + [Fact] + [Trait("Category", "D3D")] + public void DefaultModeKeepsLogAndContinueForAFailedSerializedProgram() + { + if (!OperatingSystem.IsWindows()) return; + using TempDirectory folder = new TempDirectory(); + List<(ShaderGpuProgramType Stage, byte[] Code)> programs = new List<(ShaderGpuProgramType, byte[])> + { + (ShaderGpuProgramType.DX11VertexSM40, "DXBC"u8.ToArray()), + (ShaderGpuProgramType.DX11PixelSM40, RecompileVerify.CompileHlsl("float4 main():SV_Target { return 1; }", "main", "ps_4_0")), + }; + File.WriteAllBytes(Path.Combine(folder.Path, "blob.bin"), BuildBlob(programs)); + File.WriteAllText(Path.Combine(folder.Path, "metadata.json"), JsonSerializer.Serialize(new + { + name = "Test/PartialInput", subShaders = new[] { new { passes = new[] { new + { + directXPrograms = new[] + { + new { blobIndex = 0, stage = "Vertex", keywords = Array.Empty(), keywordMask = Array.Empty() }, + new { blobIndex = 1, stage = "Fragment", keywords = Array.Empty(), keywordMask = Array.Empty() }, + }, + } } } }, + })); + DecompilationResult result = Decompiler.DecompileFolder(folder.Path, new DecompileOptions { Verbose = false }); + Assert.Equal(1, Assert.Single(result.Subprograms).Index); + Assert.Throws(() => Decompiler.DecompileFolder(folder.Path, + new DecompileOptions { RestoreKeywordVariants = true, Verbose = false })); + } + + [Fact] + [Trait("Category", "D3D")] + public void DefaultModeRetainsGeometryProgramsWithSerializedReferences() + { + if (!OperatingSystem.IsWindows()) return; + using TempDirectory folder = new TempDirectory(); + const string varying = "struct V { float4 position:SV_POSITION; float marker:TEXCOORD0; }; "; + List<(ShaderGpuProgramType Stage, byte[] Code)> programs = new List<(ShaderGpuProgramType, byte[])> + { + (ShaderGpuProgramType.DX11VertexSM40, RecompileVerify.CompileHlsl(varying + + "V main(float4 p:POSITION) { V o; o.position=p; o.marker=p.x; return o; }", "main", "vs_4_0")), + (ShaderGpuProgramType.DX11GeometrySM40, RecompileVerify.CompileHlsl(varying + + "[maxvertexcount(1)] void main(point V input[1], inout PointStream output) { output.Append(input[0]); }", "main", "gs_4_0")), + (ShaderGpuProgramType.DX11PixelSM40, RecompileVerify.CompileHlsl(varying + + "float4 main(V i):SV_Target { return i.marker; }", "main", "ps_4_0")), + }; + File.WriteAllBytes(Path.Combine(folder.Path, "blob.bin"), BuildBlob(programs)); + File.WriteAllText(Path.Combine(folder.Path, "metadata.json"), JsonSerializer.Serialize(new + { + name = "Test/GeometryVariants", subShaders = new[] { new { passes = new[] { new + { + name = "Geometry", directXPrograms = new[] + { + new { blobIndex = 0, stage = "Vertex", keywords = Array.Empty(), keywordMask = Array.Empty() }, + new { blobIndex = 1, stage = "Geometry", keywords = Array.Empty(), keywordMask = Array.Empty() }, + new { blobIndex = 2, stage = "Fragment", keywords = Array.Empty(), keywordMask = Array.Empty() }, + }, + } } } }, + })); + DecompilationResult result = Decompiler.DecompileFolder(folder.Path, + new DecompileOptions { Verbose = false, ReconstructSurfaceShaders = false }); + Assert.Equal(3, result.Subprograms.Count); + Assert.NotNull(Assert.Single(result.Passes).GeometryFunction); + Assert.NotNull(result.Passes[0].VertexFunction); + Assert.NotNull(result.Passes[0].FragmentFunction); + } + [Fact] [Trait("Category", "D3D")] public void NativeProgramsKeepTheirPassesAndSelectByKeywords() diff --git a/tests/UnityShaderDecompiler.Tests/VariantPairingTests.cs b/tests/UnityShaderDecompiler.Tests/VariantPairingTests.cs index 110ade0..298aa3c 100644 --- a/tests/UnityShaderDecompiler.Tests/VariantPairingTests.cs +++ b/tests/UnityShaderDecompiler.Tests/VariantPairingTests.cs @@ -5,6 +5,32 @@ namespace UnityShaderDecompiler.Tests; public class VariantPairingTests { + [Fact] + public void ExpandedVariantsPreserveGeometryAndTessellationStages() + { + ProgramVariantMetadata[] programs = + [ + new(0, "Vertex", [], []), + new(1, "Hull", [], ["MODE"]), new(2, "Hull", ["MODE"], ["MODE"]), + new(3, "Domain", ["MODE"], ["MODE"]), new(4, "Domain", [], ["MODE"]), + new(5, "Geometry", [], []), + new(6, "Fragment", [], ["DETAIL"]), new(7, "Fragment", ["DETAIL"], ["DETAIL"]), + ]; + List groups = VariantPairing.Group(programs); + Assert.Equal(4, groups.Count); + Assert.Contains(groups, group => group.Select(program => program.BlobIndex).SequenceEqual(new[] { 0, 1, 4, 5, 6 })); + Assert.Contains(groups, group => group.Select(program => program.BlobIndex).SequenceEqual(new[] { 0, 2, 3, 5, 7 })); + Assert.All(groups, group => Assert.Equal(new[] { "Vertex", "Hull", "Domain", "Geometry", "Fragment" }, + group.Select(program => program.Stage))); + } + + [Fact] + public void ExpandedVariantsRejectUnmatchedIntermediateStages() + { + ProgramVariantMetadata[] programs = [new(0, "Vertex", [], ["MODE"]), new(1, "Geometry", ["MODE"], ["MODE"])]; + Assert.Throws(() => VariantPairing.Group(programs)); + } + [Fact] public void FragmentOnlyKeywordReusesTheVertexProgram() { From 36f7190e281b31c00dec234ca1480ee4066d2100 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 14:47:09 +0100 Subject: [PATCH 53/93] Support default structured loads and preserve public IR contracts --- README.md | 8 ++-- .../Core/Hlsl/HlslPrettyPrinter.cs | 42 ++++++++++++----- Shader Decompiler/DXBC/IR/IRBuilder.cs | 6 ++- Shader Decompiler/DXBC/IR/IRExpression.cs | 4 +- Shader Decompiler/DXBC/IR/IRPipeline.cs | 10 +++- Shader Decompiler/Decompiler.cs | 2 +- docs/pr-structured-buffer-loads.md | 6 ++- .../RegisterRoundTrip.cs | 22 +++++++-- .../StructuredBufferLoadsTests.cs | 5 +- .../StructuredLoadCompatibilityTests.cs | 46 +++++++++++++++++++ 10 files changed, 121 insertions(+), 30 deletions(-) create mode 100644 tests/UnityShaderDecompiler.Tests/StructuredLoadCompatibilityTests.cs diff --git a/README.md b/README.md index 8baa4f9..5043470 100644 --- a/README.md +++ b/README.md @@ -320,9 +320,11 @@ storing raw bits. Integer reads remain in integer storage, preserving low-bit conditions that fxc can otherwise fold away through a float round trip. - Targets Unity 2022.3 D3D11 shader blobs. -- Structured-buffer loads require `--no-fuse-temps`. Element stride, constant - byte offsets and swizzles are preserved; fused mode, dynamic offsets and - unaligned or out-of-range accesses fail explicitly. +- Structured-buffer loads work with the default options and `--no-fuse-temps`. + Their functions automatically retain raw register storage and skip unsafe + temporary fusion. Strides, constant/dynamic byte offsets and swizzles are + preserved; invalid constant offsets fail explicitly. Existing public IR + entry points and texture-operation enum values are retained. - Vertex and fragment stages are fully reconstructed. Hull, geometry, domain, and compute programs are parsed and IR-built, but the AST/emit path mainly targets vertex/fragment; other stages attach by serialization order. diff --git a/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs b/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs index e8dfa7c..ccf2ca8 100644 --- a/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs +++ b/Shader Decompiler/Core/Hlsl/HlslPrettyPrinter.cs @@ -659,7 +659,7 @@ private static void PrintFunction( { Cbuffers = cbuffers, Stage = fn.Stage, - PreserveArithmetic = !fuseTemps, + PreserveArithmetic = !fuseTemps || fn.RawTemporaryStorage, RawTemporaryStorage = fn.RawTemporaryStorage, InputElements = fn.InputElementsByRegister, OutputElements = fn.OutputElementsByRegister, @@ -684,7 +684,7 @@ private static void PrintFunction( HlslSemanticNaming.Apply(fn.Statements, cbuffers, stageInputs, stageOutputs); - if (fuseTemps) + if (fuseTemps && !fn.RawTemporaryStorage) HlslFuseTemps.Apply(fn.Statements, MaxFuseNodes); // Buffer the body so the Unity macro defines collected during @@ -2495,6 +2495,7 @@ private static Dictionary WrittenDeclNames(List private static string RenderExpression(IRExpression expr, PrintContext ctx) => expr switch { + IRExpression.BitFieldInsertExpression insert => RenderBitFieldInsert(insert, ctx), IRExpression.IntrinsicExpression i => RenderIntrinsic(i, ctx), IRExpression.FusedMultiplyAddExpression f => $"mad({RenderExpression(f.A, ctx)}, {RenderExpression(f.B, ctx)}, {RenderExpression(f.C, ctx)})", IRExpression.MatrixVectorMultiplyExpression mv => RenderMatrixMultiply(mv, ctx), @@ -2752,6 +2753,15 @@ private static string RenderMatrixMultiply(IRExpression.MatrixVectorMultiplyExpr // Load/Sample/SampleLevel/SampleBias/SampleGrad/Gather keep the same // name DXBC uses, only the Compare variants and argument order/style // needed fixing up from the debug ToString(). + private static string RenderBitFieldInsert(IRExpression.BitFieldInsertExpression insert, PrintContext ctx) + { + // DXBC masks both counts to five bits; no shift by 32 is emitted. + string width = $"({RenderExpression(insert.Width, ctx)} & 31u)"; + string offset = $"({RenderExpression(insert.Offset, ctx)} & 31u)"; + string mask = $"(((1u << {width}) - 1u) << {offset})"; + return $"((({RenderExpression(insert.Insert, ctx)} << {offset}) & {mask}) | ({RenderExpression(insert.Base, ctx)} & ~{mask}))"; + } + private static string RenderTextureOp(IRExpression.TextureOperationExpression tex, PrintContext ctx) { if (tex.Operation == IRExpression.TextureOperation.LoadStructured) @@ -2814,13 +2824,14 @@ private static string RenderStructuredLoad(IRExpression.TextureOperationExpressi { // Float temporaries can numerically convert integer outputs and lose payload bits. if (!ctx.RawTemporaryStorage) - throw new InvalidDataException("Structured loads require --no-fuse-temps to preserve register bits."); + throw new InvalidDataException("Structured loads require raw register storage in the HLSL function."); - if (tex.Coordinates is null || tex.Offset is not IRExpression.ConstantExpression { RawValues.Length: 1 } offset) - throw new InvalidDataException("Structured load requires an element index and a constant byte offset."); - uint byteOffset = offset.RawValues[0]; - if (byteOffset % 4 != 0) - throw new InvalidDataException($"Unaligned structured load offset {byteOffset}."); + if (tex.Coordinates is null || tex.Offset is null) + throw new InvalidDataException("Structured load requires an element index and a byte offset."); + uint? byteOffset = tex.Offset is IRExpression.ConstantExpression { RawValues.Length: 1 } offset + ? offset.RawValues[0] : null; + if (byteOffset.HasValue && byteOffset.Value % 4 != 0) + throw new InvalidDataException($"Unaligned structured load offset {byteOffset.Value}."); List lanes = MaskIndices(tex.DestinationMask); if (lanes.Count == 0) @@ -2829,9 +2840,18 @@ private static string RenderStructuredLoad(IRExpression.TextureOperationExpressi List values = new List(); foreach (int lane in lanes) { - uint word = byteOffset / 4 + (uint)swizzle[lane]; - if (word >= tex.StructuredStride / 4) - throw new InvalidDataException($"Structured load exceeds element stride at t{tex.Resource.Index}."); + string word; + if (byteOffset.HasValue) + { + uint index = byteOffset.Value / 4 + (uint)swizzle[lane]; + if (index >= tex.StructuredStride / 4) + throw new InvalidDataException($"Structured load exceeds element stride at t{tex.Resource.Index}."); + word = index.ToString(System.Globalization.CultureInfo.InvariantCulture); + } + else + { + word = $"(({RenderExpression(tex.Offset, ctx)}) >> 2) + {swizzle[lane]}u"; + } values.Add($"t{tex.Resource.Index}[{RenderExpression(tex.Coordinates, ctx)}].value[{word}]"); } return lanes.Count == 1 diff --git a/Shader Decompiler/DXBC/IR/IRBuilder.cs b/Shader Decompiler/DXBC/IR/IRBuilder.cs index 9ceab4d..629ef90 100644 --- a/Shader Decompiler/DXBC/IR/IRBuilder.cs +++ b/Shader Decompiler/DXBC/IR/IRBuilder.cs @@ -6,7 +6,9 @@ public partial class IRBuilder { private readonly bool _preserveRegisterBits; - public IRBuilder(bool preserveRegisterBits = false) => _preserveRegisterBits = preserveRegisterBits; + public IRBuilder() : this(false) { } + + public IRBuilder(bool preserveRegisterBits) => _preserveRegisterBits = preserveRegisterBits; private readonly Dictionary<(RegisterType Type, uint Index), IRValueType> _registerTypes = new(); public IRProgram Build(ShdrParser parser) @@ -845,4 +847,4 @@ private IRValueType GetRegisterType(RegisterType type, uint index) return IRValueType.Unknown; } -} \ No newline at end of file +} diff --git a/Shader Decompiler/DXBC/IR/IRExpression.cs b/Shader Decompiler/DXBC/IR/IRExpression.cs index 110c896..8e0e355 100644 --- a/Shader Decompiler/DXBC/IR/IRExpression.cs +++ b/Shader Decompiler/DXBC/IR/IRExpression.cs @@ -520,7 +520,6 @@ public enum TextureOperation SampleCompare, SampleCompareLevelZero, Load, - LoadStructured, Gather, GatherCompare, Lod, @@ -529,6 +528,7 @@ public enum TextureOperation SamplePos, BufInfo, CheckAccessFullyMapped, + LoadStructured, } public sealed class TextureOperationExpression : IRExpression @@ -697,4 +697,4 @@ public static IEnumerable CollectRegisterUses(this IRExpression? exp break; } } -} \ No newline at end of file +} diff --git a/Shader Decompiler/DXBC/IR/IRPipeline.cs b/Shader Decompiler/DXBC/IR/IRPipeline.cs index e9bc053..876da9e 100644 --- a/Shader Decompiler/DXBC/IR/IRPipeline.cs +++ b/Shader Decompiler/DXBC/IR/IRPipeline.cs @@ -12,6 +12,7 @@ public static class IRPipeline { public sealed class Result { + public bool PreserveRegisterBits { get; init; } public required IRProgram Program { get; init; } public required List Blocks { get; init; } public required IRSsaVerificationResult SsaVerification { get; init; } @@ -24,8 +25,12 @@ public sealed class Result public List<(string Phase, string Text)> StageDumps { get; } = new(); } - public static Result Run(DxbcFile file, bool dumpStages = false, bool preserveArithmetic = false) + public static Result Run(DxbcFile file, bool dumpStages = false) => Run(file, dumpStages, false); + + public static Result Run(DxbcFile file, bool dumpStages, bool preserveArithmetic) { + // Structured resources carry raw words regardless of the requested temp optimization. + preserveArithmetic |= file.Shader!.Instructions.Any(instruction => instruction.Opcode == Instructions.Opcode.LdStructured); var stageDumps = new List<(string Phase, string Text)>(); void Snapshot(string phase, List blocks) @@ -112,6 +117,7 @@ void Snapshot(string phase, List blocks) var result = new Result { + PreserveRegisterBits = preserveArithmetic, Program = program, Blocks = blocks, SsaVerification = ssaResult, @@ -120,4 +126,4 @@ void Snapshot(string phase, List blocks) result.StageDumps.AddRange(stageDumps); return result; } -} \ No newline at end of file +} diff --git a/Shader Decompiler/Decompiler.cs b/Shader Decompiler/Decompiler.cs index 7dce58d..eb0d6ac 100644 --- a/Shader Decompiler/Decompiler.cs +++ b/Shader Decompiler/Decompiler.cs @@ -314,7 +314,7 @@ public static DecompilationResult Decompile( } else { - function.RawTemporaryStorage = !options.FuseTemps; + function.RawTemporaryStorage = pipelineResult.PreserveRegisterBits; candidates.Add(new DecompiledSubprogram { Index = i, diff --git a/docs/pr-structured-buffer-loads.md b/docs/pr-structured-buffer-loads.md index 69e9753..1576c36 100644 --- a/docs/pr-structured-buffer-loads.md +++ b/docs/pr-structured-buffer-loads.md @@ -2,10 +2,12 @@ Float temporary storage can corrupt integer moves and structured-buffer results. With `--no-fuse-temps`, preserve raw uint storage, typed reads and outputs, full-bit comparison masks and integer source modifiers. Keep arithmetic rewrites from removing the addition before fractional-part evaluation. -Structured loads preserve element stride, byte offset and resource swizzle independently. These changes belong together because correct addressing alone still loses integer output bits without raw register transport. Structured loads require `--no-fuse-temps`; fused mode, dynamic offsets and unaligned or out-of-range accesses fail explicitly. This PR includes the former #7; no separate dependency remains. +Structured loads preserve element stride, byte offset and resource swizzle independently. These changes belong together because correct addressing alone still loses integer output bits without raw register transport. Default options now select raw storage and skip unsafe fusion for functions containing structured loads; `--no-fuse-temps` remains supported. Constant and dynamic byte offsets work, including bit-field address calculations emitted by fxc. Invalid constant offsets fail explicitly. This PR includes the former #7; no separate dependency remains. + +Compatibility: retain the parameterless `IRBuilder` constructor, the original two-parameter `IRPipeline.Run` overload and existing texture-operation enum values. New overloads and enum members are additive. Saturated moves clamp the floating-point value before storing its bits. Integer reads bypass float round trips that fxc can fold to zero for known subnormals; this preserves low-bit branch conditions. Native round trips cover signed/unsigned moves, saturation and low-bit selection, plus float/uint/int structured loads with stride, offset and swizzle. A restricted bytecode evaluator checks exact output words, including NaN payloads, subnormals and signed zero, and rejects unexpected operations. The saturation regression fails with the former implementation and passes with the fix. These are compiler/bytecode checks, not GPU execution tests. -Validation: 67 C# tests and all four PicaVoxel end-to-end cases pass on Windows. The isolated suite uses en-US because upstream's fr-FR golden-output failure is addressed separately in #5. The README documents both raw register preservation and the structured-load restriction. +Validation: 72 C# tests and all four PicaVoxel cases pass on Windows. New regressions fail before the compatibility fix and pass afterward, covering default options, dynamic offsets and the old API. Isolated C# tests use en-US; the upstream fr-FR golden-output failure is addressed separately in #5. diff --git a/tests/UnityShaderDecompiler.Tests/RegisterRoundTrip.cs b/tests/UnityShaderDecompiler.Tests/RegisterRoundTrip.cs index 24eb9a3..97620f9 100644 --- a/tests/UnityShaderDecompiler.Tests/RegisterRoundTrip.cs +++ b/tests/UnityShaderDecompiler.Tests/RegisterRoundTrip.cs @@ -24,12 +24,12 @@ internal static DxbcFile Compile(string source, string entry = "main") internal static DxbcFile Recompile(DxbcFile original, bool fuseTemps = false) { - IRPipeline.Result pipeline = IRPipeline.Run(original, preserveArithmetic: !fuseTemps); + IRPipeline.Result pipeline = IRPipeline.Run(original, dumpStages: false, preserveArithmetic: !fuseTemps); Assert.True(pipeline.SsaVerification.IsValid); HlslFunctionNode function = HlslAstBuilder.BuildFunction("RoundTrip", ShaderGpuProgramType.DX11PixelSM50, pipeline.Program.Declarations, pipeline.Blocks, original.InputSignature, original.OutputSignature)!; - function.RawTemporaryStorage = !fuseTemps; + function.RawTemporaryStorage = pipeline.PreserveRegisterBits; HlslPassNode pass = new HlslPassNode { FragmentFunction = function }; pass.Structs.Add(function.InputStruct!); pass.Structs.Add(function.OutputStruct!); @@ -41,7 +41,8 @@ internal static DxbcFile Recompile(DxbcFile original, bool fuseTemps = false) shader.SubShaders.Add(subShader); string shaderText = HlslPrettyPrinter.Print(shader, fuseTemps: fuseTemps); string hlsl = Regex.Match(shaderText, @"HLSLPROGRAM(.*?)ENDHLSL", RegexOptions.Singleline).Groups[1].Value; - return Compile(hlsl, function.Name); + try { return Compile(hlsl, function.Name); } + catch (InvalidOperationException error) { throw new InvalidOperationException(hlsl, error); } } // Deliberately limited to the bit-copy fixtures below; unexpected arithmetic fails. @@ -56,7 +57,8 @@ internal static uint[] Evaluate(DxbcFile file, uint[] input, uint[]? structuredW if (instruction.Opcode.ToString().StartsWith("Dcl", StringComparison.Ordinal)) continue; if (instruction.Opcode == Opcode.Ret) return registers[(RegisterType.Output, 0)]; Assert.True(instruction.Opcode is Opcode.Mov or Opcode.MovC or Opcode.INe or Opcode.IEq - or Opcode.And or Opcode.LdStructured, $"Unexpected opcode: {instruction.Opcode}"); + or Opcode.And or Opcode.LdStructured or Opcode.IAdd or Opcode.Ishl or Opcode.Ushr or Opcode.Bfi, + $"Unexpected opcode: {instruction.Opcode}"); Operand destination = instruction.Operands[0]; uint[] result = new uint[4]; for (int lane = 0; lane < 4; lane++) @@ -66,6 +68,15 @@ internal static uint[] Evaluate(DxbcFile file, uint[] input, uint[]? structuredW if (instruction.Opcode == Opcode.Mov) result[lane] = first; else if (instruction.Opcode == Opcode.MovC) result[lane] = Read(instruction.Operands[first != 0 ? 2 : 3], lane, registers); + else if (instruction.Opcode == Opcode.Bfi) + { + int width = (int)(first & 31); + int offset = (int)(Read(instruction.Operands[2], lane, registers) & 31); + uint mask = unchecked(((1u << width) - 1u) << offset); + uint insert = Read(instruction.Operands[3], lane, registers); + uint original = Read(instruction.Operands[4], lane, registers); + result[lane] = ((insert << offset) & mask) | (original & ~mask); + } else if (instruction.Opcode == Opcode.LdStructured) { Assert.NotNull(structuredWords); @@ -86,6 +97,9 @@ internal static uint[] Evaluate(DxbcFile file, uint[] input, uint[]? structuredW Opcode.INe => first != second ? uint.MaxValue : 0u, Opcode.IEq => first == second ? uint.MaxValue : 0u, Opcode.And => first & second, + Opcode.IAdd => unchecked(first + second), + Opcode.Ishl => first << (int)(second & 31), + Opcode.Ushr => first >> (int)(second & 31), _ => throw new InvalidOperationException(), }; } diff --git a/tests/UnityShaderDecompiler.Tests/StructuredBufferLoadsTests.cs b/tests/UnityShaderDecompiler.Tests/StructuredBufferLoadsTests.cs index 4fd7253..9592d05 100644 --- a/tests/UnityShaderDecompiler.Tests/StructuredBufferLoadsTests.cs +++ b/tests/UnityShaderDecompiler.Tests/StructuredBufferLoadsTests.cs @@ -27,9 +27,7 @@ public void StructuredLoadPreservesWordsThroughNativeRoundTrip(string type) "{ return data[index].value.zxyw; }"; Parser.DXBC.DxbcFile original = RegisterRoundTrip.Compile(source); Assert.Contains(original.Shader!.Instructions, instruction => instruction.Opcode == Opcode.LdStructured); - System.IO.InvalidDataException error = Assert.Throws( - () => RegisterRoundTrip.Recompile(original, fuseTemps: true)); - Assert.Contains("--no-fuse-temps", error.Message); + Parser.DXBC.DxbcFile defaultRebuilt = RegisterRoundTrip.Recompile(original, fuseTemps: true); Parser.DXBC.DxbcFile rebuilt = RegisterRoundTrip.Recompile(original); Assert.Equal(original.OutputSignature!.Elements[0].ComponentType, rebuilt.OutputSignature!.Elements[0].ComponentType); @@ -44,6 +42,7 @@ public void StructuredLoadPreservesWordsThroughNativeRoundTrip(string type) uint[] expected = { words[offset + 2], words[offset], words[offset + 1], words[offset + 3] }; Assert.Equal(expected, RegisterRoundTrip.Evaluate(original, input, words)); Assert.Equal(expected, RegisterRoundTrip.Evaluate(rebuilt, input, words)); + Assert.Equal(expected, RegisterRoundTrip.Evaluate(defaultRebuilt, input, words)); } } diff --git a/tests/UnityShaderDecompiler.Tests/StructuredLoadCompatibilityTests.cs b/tests/UnityShaderDecompiler.Tests/StructuredLoadCompatibilityTests.cs new file mode 100644 index 0000000..10fa1be --- /dev/null +++ b/tests/UnityShaderDecompiler.Tests/StructuredLoadCompatibilityTests.cs @@ -0,0 +1,46 @@ +using Parser.DXBC; +using Parser.DXBC.Instructions; +using Parser.DXBC.IR; + +namespace UnityShaderDecompiler.Tests; + +public sealed class StructuredLoadCompatibilityTests +{ + [Fact] + public void ExistingPublicEntryPointsAndEnumValuesRemainAvailable() + { + Assert.NotNull(typeof(IRBuilder).GetConstructor(Type.EmptyTypes)); + Assert.NotNull(typeof(IRPipeline).GetMethod("Run", new[] { typeof(DxbcFile), typeof(bool) })); + Assert.Equal(7, (int)IRExpression.TextureOperation.Gather); + Assert.Equal(14, (int)IRExpression.TextureOperation.CheckAccessFullyMapped); + } + + [Theory] + [InlineData(true, false)] + [InlineData(false, false)] + [InlineData(true, true)] + [InlineData(false, true)] + [Trait("Category", "D3D")] + public void StructuredLoadsKeepExactWordsWithDefaultOptionsAndDynamicOffsets(bool fuseTemps, bool dynamicOffset) + { + if (!OperatingSystem.IsWindows()) return; + string member = dynamicOffset ? "i.y & 3" : "2"; + string source = "struct Element { uint value[4]; }; StructuredBuffer data : register(t7); " + + $"uint4 main(nointerpolation uint2 i : TEXCOORD0) : SV_Target {{ return data[i.x].value[{member}]; }}"; + DxbcFile original = RegisterRoundTrip.Compile(source); + if (dynamicOffset) + Assert.Contains(original.Shader!.Instructions, instruction => instruction.Opcode == Opcode.LdStructured + && instruction.Operands[2].RegisterType != RegisterType.Immediate32); + DxbcFile rebuilt = RegisterRoundTrip.Recompile(original, fuseTemps); + uint[] words = { 1, 0xffffffffu, 0x7fc12345u, 0x80000000u, 0x01000001u, 0xffc54321u, 0x7fffffffu, 0x80000001u }; + for (uint index = 0; index < 2; index++) + for (uint component = 0; component < 4; component++) + { + uint[] input = { index, component, 0, 0 }; + uint expected = words[index * 4 + (dynamicOffset ? component : 2)]; + uint[] output = { expected, expected, expected, expected }; + Assert.Equal(output, RegisterRoundTrip.Evaluate(original, input, words)); + Assert.Equal(output, RegisterRoundTrip.Evaluate(rebuilt, input, words)); + } + } +} From 54cd5b460f0a278516cdbfc56a907345d4919b61 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 14:47:09 +0100 Subject: [PATCH 54/93] Preserve the public texture recognition entry point --- README.md | 2 ++ .../Core/Optimizations/IRTexturePatternRecognition.cs | 9 +++++++++ docs/pr-explicit-texture-lod.md | 2 +- .../ExplicitTextureLodTests.cs | 3 +++ 4 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 Shader Decompiler/Core/Optimizations/IRTexturePatternRecognition.cs diff --git a/README.md b/README.md index dae79fc..f51e774 100644 --- a/README.md +++ b/README.md @@ -234,6 +234,8 @@ Each subprogram goes through `IRPipeline` (`DXBC/IR/IRPipeline.cs`): 8. **Pattern recognition** — fold shader idioms back into high-level IR with the matrix, vector, texture, loop, and shader-pattern recognizers. Explicit texture LODs, including zero, remain explicit in emitted HLSL. + The public `IRTexturePatternRecognition.Run` entry point remains available + as a no-op for existing callers. 9. **Leave SSA** — eliminate phis and coalesce copies. 10. **Metadata binding** — attach RDEF/ISGN/OSGN symbolic names to the IR. 11. **AST build** — one function node per subprogram; the diff --git a/Shader Decompiler/Core/Optimizations/IRTexturePatternRecognition.cs b/Shader Decompiler/Core/Optimizations/IRTexturePatternRecognition.cs new file mode 100644 index 0000000..259c0f5 --- /dev/null +++ b/Shader Decompiler/Core/Optimizations/IRTexturePatternRecognition.cs @@ -0,0 +1,9 @@ +using Parser.DXBC.IR; + +namespace Parser.Core.Optimizations; + +// Retain the public pass entry point; explicit LOD must not become implicit sampling. +public static class IRTexturePatternRecognition +{ + public static bool Run(List blocks) => false; +} diff --git a/docs/pr-explicit-texture-lod.md b/docs/pr-explicit-texture-lod.md index 24ca74c..132065f 100644 --- a/docs/pr-explicit-texture-lod.md +++ b/docs/pr-explicit-texture-lod.md @@ -1,5 +1,5 @@ # Keep explicit zero texture LOD explicit -An explicit texture LOD of zero was emitted as implicit sampling, changing mip selection. Preserve `SampleLevel(..., 0)` when the source instruction specifies an explicit LOD. +An explicit texture LOD of zero was emitted as implicit sampling, changing mip selection. Preserve `SampleLevel(..., 0)` when the source instruction specifies an explicit LOD. Keep the public `IRTexturePatternRecognition.Run` entry point as a no-op so existing callers still compile and link. Validation: 52 C# tests and all four existing PicaVoxel extraction/decompilation tests pass. C# tests use an en-US default culture; the upstream fr-FR golden-output failure is addressed independently by the numeric-formatting PR. The golden output was regenerated by the emitter for this change only; existing comparison assertions are unchanged. diff --git a/tests/UnityShaderDecompiler.Tests/ExplicitTextureLodTests.cs b/tests/UnityShaderDecompiler.Tests/ExplicitTextureLodTests.cs index 036097d..ab2e288 100644 --- a/tests/UnityShaderDecompiler.Tests/ExplicitTextureLodTests.cs +++ b/tests/UnityShaderDecompiler.Tests/ExplicitTextureLodTests.cs @@ -25,6 +25,9 @@ public void ExplicitZeroLodIsNotReplacedByImplicitDerivatives() IRBlock block = new IRBlock(); block.Statements.Add(new IRStatement.IRAssignment { Destination = Temp(0), Expression = sample }); + Assert.False(IRTexturePatternRecognition.Run(new List { block })); + Assert.Same(sample, Assert.IsType(block.Statements[0]).Expression); + IRShaderPatternRecognition.Run(new() { block }); IRStatement.IRAssignment assignment = Assert.IsType(block.Statements[0]); From beece4241af0d2880a2e88af1af30ccc4ae3a736 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 14:50:01 +0100 Subject: [PATCH 55/93] Distinguish variant fixes from opt-in runtime improvements --- docs/pr-serialized-variant-pairing.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/pr-serialized-variant-pairing.md b/docs/pr-serialized-variant-pairing.md index 7abee99..6284543 100644 --- a/docs/pr-serialized-variant-pairing.md +++ b/docs/pr-serialized-variant-pairing.md @@ -1,4 +1,7 @@ -# Restore runtime keyword selection from serialized variants +# Restore runtime keyword selection from serialized variants + +- **Bug fix:** Match serialized stage references and keyword states, retain all graphics stages in expanded mode, and preserve constant-buffer sizes. +- **Improvement:** Add opt-in runtime keyword selection with local/global scope and an exhaustive compilation manifest. Use serialized program references and stage keyword masks to pair vertex/fragment programs. Preserve stage-only keywords and reject missing programs, unmatched states and incompatible interfaces. Keep constant-buffer sizes in extracted metadata. From 810db8a8034d35ab46d9bf1523cf3e9687c3c7b3 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 15:02:52 +0100 Subject: [PATCH 56/93] test: add licensed VolumetricLights regression corpus --- .github/workflows/volumetric-lights.yml | 20 + README.md | 4 + docs/volumetric-lights-pr.md | 10 + .../Fixtures/VolumetricLights/.gitattributes | 4 + .../VolumetricLights/BuildFixtures.cs | 48 + .../Imported/BilateralBlur.shader | 526 ++ .../VolumetricLights/Imported/BlitAdd.shader | 92 + .../Imported/VolumetricLight.shader | 648 ++ .../VolumetricLights/Json/BilateralBlur.json | 7930 +++++++++++++++++ .../VolumetricLights/Json/BlitAdd.json | 755 ++ .../Json/VolumetricLight.json | 5972 +++++++++++++ tests/Fixtures/VolumetricLights/LICENSE | 27 + tests/Fixtures/VolumetricLights/README.md | 32 + .../Source/BilateralBlur.shader | 524 ++ .../VolumetricLights/Source/BlitAdd.shader | 90 + .../Source/VolumetricLight.shader | 644 ++ tests/Fixtures/VolumetricLights/manifest.json | 18 + tests/e2e/test_volumetric_lights.py | 57 + 18 files changed, 17401 insertions(+) create mode 100644 .github/workflows/volumetric-lights.yml create mode 100644 docs/volumetric-lights-pr.md create mode 100644 tests/Fixtures/VolumetricLights/.gitattributes create mode 100644 tests/Fixtures/VolumetricLights/BuildFixtures.cs create mode 100644 tests/Fixtures/VolumetricLights/Imported/BilateralBlur.shader create mode 100644 tests/Fixtures/VolumetricLights/Imported/BlitAdd.shader create mode 100644 tests/Fixtures/VolumetricLights/Imported/VolumetricLight.shader create mode 100644 tests/Fixtures/VolumetricLights/Json/BilateralBlur.json create mode 100644 tests/Fixtures/VolumetricLights/Json/BlitAdd.json create mode 100644 tests/Fixtures/VolumetricLights/Json/VolumetricLight.json create mode 100644 tests/Fixtures/VolumetricLights/LICENSE create mode 100644 tests/Fixtures/VolumetricLights/README.md create mode 100644 tests/Fixtures/VolumetricLights/Source/BilateralBlur.shader create mode 100644 tests/Fixtures/VolumetricLights/Source/BlitAdd.shader create mode 100644 tests/Fixtures/VolumetricLights/Source/VolumetricLight.shader create mode 100644 tests/Fixtures/VolumetricLights/manifest.json create mode 100644 tests/e2e/test_volumetric_lights.py diff --git a/.github/workflows/volumetric-lights.yml b/.github/workflows/volumetric-lights.yml new file mode 100644 index 0000000..ac68ef1 --- /dev/null +++ b/.github/workflows/volumetric-lights.yml @@ -0,0 +1,20 @@ +name: VolumetricLights regression +on: + pull_request: + workflow_dispatch: +permissions: + contents: read +jobs: + regression: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - run: python -m pip install lz4 + - run: dotnet build src/UnityShaderDecompiler.Cli/UnityShaderDecompiler.Cli.csproj -c Release + - run: python tests/e2e/test_volumetric_lights.py diff --git a/README.md b/README.md index a24e1dc..7919e2f 100644 --- a/README.md +++ b/README.md @@ -409,6 +409,10 @@ If you change the pretty printer or surface-shader recognition, keep - The reverse-engineering notes in `docs/` document the UnityShaderCompiler work this project is built on. +## VolumetricLights regression corpus + +The [VolumetricLights fixtures](tests/Fixtures/VolumetricLights/README.md) add three BSD-licensed shaders compiled with Unity 6000.0.75f1 / D3D11. Run `python tests/e2e/test_volumetric_lights.py` after building the Release CLI. This draft corpus currently exposes upstream JSON-schema and Unity 6 blob-reading failures; it does not claim rendering equivalence. + ## License Apache License 2.0 — see [LICENSE](LICENSE). diff --git a/docs/volumetric-lights-pr.md b/docs/volumetric-lights-pr.md new file mode 100644 index 0000000..192f4b3 --- /dev/null +++ b/docs/volumetric-lights-pr.md @@ -0,0 +1,10 @@ +## Improvement + +Add three BSD-licensed VolumetricLights shaders as an offline end-to-end regression corpus, with pinned source provenance, Unity 6000.0.75f1 D3D11 exports, migration snapshots, and integrity hashes. No production behavior changes. + +## Validation / blockers + +- All three input shaders compile and the asset bundle builds with Unity CLI 6000.0.75f1 / D3D11. +- Fixture integrity passes; all three extraction tests currently fail on upstream `417f26b` (`m_CompressedBlob` schema mismatch). Requires the schema support from #6. +- A diagnostic extraction with schema support reveals additional swallowed Unity 6 blob-reading errors in upstream. Keep this PR in draft; do not relax the assertions to accept partial output. +- These tests do not prove rendered equivalence or runtime keyword selection. diff --git a/tests/Fixtures/VolumetricLights/.gitattributes b/tests/Fixtures/VolumetricLights/.gitattributes new file mode 100644 index 0000000..191deef --- /dev/null +++ b/tests/Fixtures/VolumetricLights/.gitattributes @@ -0,0 +1,4 @@ +# Preserve upstream sources, importer snapshots, and exports byte-for-byte. +Source/*.shader -text whitespace=-blank-at-eol +Imported/*.shader -text whitespace=-blank-at-eol +Json/*.json -text whitespace=cr-at-eol diff --git a/tests/Fixtures/VolumetricLights/BuildFixtures.cs b/tests/Fixtures/VolumetricLights/BuildFixtures.cs new file mode 100644 index 0000000..7cf5fe8 --- /dev/null +++ b/tests/Fixtures/VolumetricLights/BuildFixtures.cs @@ -0,0 +1,48 @@ +using System; +using System.IO; +using System.Linq; +using UnityEditor; +using UnityEngine; +using UnityEngine.Rendering; + +// Copy into Assets/Editor in a disposable Built-in project. +public static class BuildFixtures +{ + public static void Run() + { + File.WriteAllText("fixture-build.json", "{\"passed\":false}"); + if (Application.unityVersion != "6000.0.75f1") + throw new InvalidOperationException("Fixture generation requires Unity 6000.0.75f1."); + if (SystemInfo.graphicsDeviceType != GraphicsDeviceType.Direct3D11) + throw new InvalidOperationException("Fixture generation requires Direct3D 11."); + string[] assets = Directory.GetFiles("Assets/VolumetricLights", "*.shader") + .Select(path => path.Replace('\\', '/')).OrderBy(path => path, StringComparer.Ordinal).ToArray(); + if (assets.Length != 3) throw new InvalidOperationException("Expected the three upstream shaders."); + PlayerSettings.SetUseDefaultGraphicsAPIs(BuildTarget.StandaloneWindows64, false); + PlayerSettings.SetGraphicsAPIs(BuildTarget.StandaloneWindows64, new[] { GraphicsDeviceType.Direct3D11 }); + SerializedObject graphics = new SerializedObject(AssetDatabase.LoadAllAssetsAtPath("ProjectSettings/GraphicsSettings.asset")[0]); + SerializedProperty included = graphics.FindProperty("m_AlwaysIncludedShaders"); + included.arraySize = assets.Length; + for (int index = 0; index < assets.Length; index++) + { + AssetDatabase.ImportAsset(assets[index], ImportAssetOptions.ForceSynchronousImport); + Shader shader = AssetDatabase.LoadAssetAtPath(assets[index]); + if (shader == null || ShaderUtil.ShaderHasError(shader)) + throw new InvalidOperationException("Shader import failed: " + assets[index]); + included.GetArrayElementAtIndex(index).objectReferenceValue = shader; + } + graphics.ApplyModifiedPropertiesWithoutUndo(); + AssetDatabase.SaveAssets(); + Directory.CreateDirectory("FixtureBundle"); + AssetBundleBuild bundle = new AssetBundleBuild { assetBundleName = "volumetriclights", assetNames = assets }; + AssetBundleManifest manifest = BuildPipeline.BuildAssetBundles("FixtureBundle", new[] { bundle }, + BuildAssetBundleOptions.ForceRebuildAssetBundle | BuildAssetBundleOptions.StrictMode, + BuildTarget.StandaloneWindows64); + if (manifest == null || !File.Exists("FixtureBundle/volumetriclights")) + throw new InvalidOperationException("Fixture bundle compilation failed."); + foreach (string path in assets) + if (ShaderUtil.ShaderHasError(AssetDatabase.LoadAssetAtPath(path))) + throw new InvalidOperationException("Shader compilation failed: " + path); + File.WriteAllText("fixture-build.json", "{\"passed\":true,\"editor\":\"6000.0.75f1\",\"backend\":\"Direct3D11\",\"shaders\":3}"); + } +} diff --git a/tests/Fixtures/VolumetricLights/Imported/BilateralBlur.shader b/tests/Fixtures/VolumetricLights/Imported/BilateralBlur.shader new file mode 100644 index 0000000..0c7c619 --- /dev/null +++ b/tests/Fixtures/VolumetricLights/Imported/BilateralBlur.shader @@ -0,0 +1,526 @@ +// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)' + +// Copyright(c) 2016, Michal Skalsky +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors +// may be used to endorse or promote products derived from this software without +// specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +// OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.IN NO EVENT +// SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT +// OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +// TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +// EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + +Shader "Hidden/BilateralBlur" +{ + Properties + { + _MainTex("Texture", any) = "" {} + } + SubShader + { + // No culling or depth + Cull Off ZWrite Off ZTest Always + + CGINCLUDE + + //-------------------------------------------------------------------------------------------- + // Downsample, bilateral blur and upsample config + //-------------------------------------------------------------------------------------------- + // method used to downsample depth buffer: 0 = min; 1 = max; 2 = min/max in chessboard pattern + #define DOWNSAMPLE_DEPTH_MODE 2 + #define UPSAMPLE_DEPTH_THRESHOLD 1.5f + #define BLUR_DEPTH_FACTOR 0.5 + #define GAUSS_BLUR_DEVIATION 1.5 + #define FULL_RES_BLUR_KERNEL_SIZE 7 + #define HALF_RES_BLUR_KERNEL_SIZE 5 + #define QUARTER_RES_BLUR_KERNEL_SIZE 6 + //-------------------------------------------------------------------------------------------- + + #define PI 3.1415927f + +#include "UnityCG.cginc" + + UNITY_DECLARE_TEX2D(_CameraDepthTexture); + UNITY_DECLARE_TEX2D(_HalfResDepthBuffer); + UNITY_DECLARE_TEX2D(_QuarterResDepthBuffer); + UNITY_DECLARE_TEX2D(_HalfResColor); + UNITY_DECLARE_TEX2D(_QuarterResColor); + UNITY_DECLARE_TEX2D(_MainTex); + + float4 _CameraDepthTexture_TexelSize; + float4 _HalfResDepthBuffer_TexelSize; + float4 _QuarterResDepthBuffer_TexelSize; + + struct appdata + { + float4 vertex : POSITION; + float2 uv : TEXCOORD0; + }; + + struct v2f + { + float2 uv : TEXCOORD0; + float4 vertex : SV_POSITION; + }; + + struct v2fDownsample + { +#if SHADER_TARGET > 40 + float2 uv : TEXCOORD0; +#else + float2 uv00 : TEXCOORD0; + float2 uv01 : TEXCOORD1; + float2 uv10 : TEXCOORD2; + float2 uv11 : TEXCOORD3; +#endif + float4 vertex : SV_POSITION; + }; + + struct v2fUpsample + { + float2 uv : TEXCOORD0; + float2 uv00 : TEXCOORD1; + float2 uv01 : TEXCOORD2; + float2 uv10 : TEXCOORD3; + float2 uv11 : TEXCOORD4; + float4 vertex : SV_POSITION; + }; + + v2f vert(appdata v) + { + v2f o; + o.vertex = UnityObjectToClipPos(v.vertex); + o.uv = v.uv; + return o; + } + + //----------------------------------------------------------------------------------------- + // vertDownsampleDepth + //----------------------------------------------------------------------------------------- + v2fDownsample vertDownsampleDepth(appdata v, float2 texelSize) + { + v2fDownsample o; + o.vertex = UnityObjectToClipPos(v.vertex); +#if SHADER_TARGET > 40 + o.uv = v.uv; +#else + o.uv00 = v.uv - 0.5 * texelSize.xy; + o.uv10 = o.uv00 + float2(texelSize.x, 0); + o.uv01 = o.uv00 + float2(0, texelSize.y); + o.uv11 = o.uv00 + texelSize.xy; +#endif + return o; + } + + //----------------------------------------------------------------------------------------- + // vertUpsample + //----------------------------------------------------------------------------------------- + v2fUpsample vertUpsample(appdata v, float2 texelSize) + { + v2fUpsample o; + o.vertex = UnityObjectToClipPos(v.vertex); + o.uv = v.uv; + + o.uv00 = v.uv - 0.5 * texelSize.xy; + o.uv10 = o.uv00 + float2(texelSize.x, 0); + o.uv01 = o.uv00 + float2(0, texelSize.y); + o.uv11 = o.uv00 + texelSize.xy; + return o; + } + + //----------------------------------------------------------------------------------------- + // BilateralUpsample + //----------------------------------------------------------------------------------------- + float4 BilateralUpsample(v2fUpsample input, Texture2D hiDepth, Texture2D loDepth, Texture2D loColor, SamplerState linearSampler, SamplerState pointSampler) + { + const float threshold = UPSAMPLE_DEPTH_THRESHOLD; + float4 highResDepth = LinearEyeDepth(hiDepth.Sample(pointSampler, input.uv)).xxxx; + float4 lowResDepth; + + lowResDepth[0] = LinearEyeDepth(loDepth.Sample(pointSampler, input.uv00)); + lowResDepth[1] = LinearEyeDepth(loDepth.Sample(pointSampler, input.uv10)); + lowResDepth[2] = LinearEyeDepth(loDepth.Sample(pointSampler, input.uv01)); + lowResDepth[3] = LinearEyeDepth(loDepth.Sample(pointSampler, input.uv11)); + + float4 depthDiff = abs(lowResDepth - highResDepth); + + float accumDiff = dot(depthDiff, float4(1, 1, 1, 1)); + + [branch] + if (accumDiff < threshold) // small error, not an edge -> use bilinear filter + { + return loColor.Sample(linearSampler, input.uv); + } + + // find nearest sample + float minDepthDiff = depthDiff[0]; + float2 nearestUv = input.uv00; + + if (depthDiff[1] < minDepthDiff) + { + nearestUv = input.uv10; + minDepthDiff = depthDiff[1]; + } + + if (depthDiff[2] < minDepthDiff) + { + nearestUv = input.uv01; + minDepthDiff = depthDiff[2]; + } + + if (depthDiff[3] < minDepthDiff) + { + nearestUv = input.uv11; + minDepthDiff = depthDiff[3]; + } + + return loColor.Sample(pointSampler, nearestUv); + } + + //----------------------------------------------------------------------------------------- + // DownsampleDepth + //----------------------------------------------------------------------------------------- + float DownsampleDepth(v2fDownsample input, Texture2D depthTexture, SamplerState depthSampler) + { +#if SHADER_TARGET > 40 + float4 depth = depthTexture.Gather(depthSampler, input.uv); +#else + float4 depth; + depth.x = depthTexture.Sample(depthSampler, input.uv00).x; + depth.y = depthTexture.Sample(depthSampler, input.uv01).x; + depth.z = depthTexture.Sample(depthSampler, input.uv10).x; + depth.w = depthTexture.Sample(depthSampler, input.uv11).x; + +#endif + +#if DOWNSAMPLE_DEPTH_MODE == 0 // min depth + return min(min(depth.x, depth.y), min(depth.z, depth.w)); +#elif DOWNSAMPLE_DEPTH_MODE == 1 // max depth + return max(max(depth.x, depth.y), max(depth.z, depth.w)); +#elif DOWNSAMPLE_DEPTH_MODE == 2 // min/max depth in chessboard pattern + + float minDepth = min(min(depth.x, depth.y), min(depth.z, depth.w)); + float maxDepth = max(max(depth.x, depth.y), max(depth.z, depth.w)); + + // chessboard pattern + int2 position = input.vertex.xy % 2; + int index = position.x + position.y; + return index == 1 ? minDepth : maxDepth; +#endif + } + + //----------------------------------------------------------------------------------------- + // GaussianWeight + //----------------------------------------------------------------------------------------- + float GaussianWeight(float offset, float deviation) + { + float weight = 1.0f / sqrt(2.0f * PI * deviation * deviation); + weight *= exp(-(offset * offset) / (2.0f * deviation * deviation)); + return weight; + } + + //----------------------------------------------------------------------------------------- + // BilateralBlur + //----------------------------------------------------------------------------------------- + float4 BilateralBlur(v2f input, int2 direction, Texture2D depth, SamplerState depthSampler, const int kernelRadius, float2 pixelSize) + { + //const float deviation = kernelRadius / 2.5; + const float deviation = kernelRadius / GAUSS_BLUR_DEVIATION; // make it really strong + + float2 uv = input.uv; + float4 centerColor = _MainTex.Sample(sampler_MainTex, uv); + float3 color = centerColor.xyz; + //return float4(color, 1); + float centerDepth = (LinearEyeDepth(depth.Sample(depthSampler, uv))); + + float weightSum = 0; + + // gaussian weight is computed from constants only -> will be computed in compile time + float weight = GaussianWeight(0, deviation); + color *= weight; + weightSum += weight; + + [unroll] for (int i = -kernelRadius; i < 0; i += 1) + { + float2 offset = (direction * i); + float3 sampleColor = _MainTex.Sample(sampler_MainTex, input.uv, offset); + float sampleDepth = (LinearEyeDepth(depth.Sample(depthSampler, input.uv, offset))); + + float depthDiff = abs(centerDepth - sampleDepth); + float dFactor = depthDiff * BLUR_DEPTH_FACTOR; + float w = exp(-(dFactor * dFactor)); + + // gaussian weight is computed from constants only -> will be computed in compile time + weight = GaussianWeight(i, deviation) * w; + + color += weight * sampleColor; + weightSum += weight; + } + + [unroll] for (i = 1; i <= kernelRadius; i += 1) + { + float2 offset = (direction * i); + float3 sampleColor = _MainTex.Sample(sampler_MainTex, input.uv, offset); + float sampleDepth = (LinearEyeDepth(depth.Sample(depthSampler, input.uv, offset))); + + float depthDiff = abs(centerDepth - sampleDepth); + float dFactor = depthDiff * BLUR_DEPTH_FACTOR; + float w = exp(-(dFactor * dFactor)); + + // gaussian weight is computed from constants only -> will be computed in compile time + weight = GaussianWeight(i, deviation) * w; + + color += weight * sampleColor; + weightSum += weight; + } + + color /= weightSum; + return float4(color, centerColor.w); + } + + ENDCG + + // pass 0 - horizontal blur (hires) + Pass + { + CGPROGRAM + #pragma vertex vert + #pragma fragment horizontalFrag + #pragma target 4.0 + + fixed4 horizontalFrag(v2f input) : SV_Target + { + return BilateralBlur(input, int2(1, 0), _CameraDepthTexture, sampler_CameraDepthTexture, FULL_RES_BLUR_KERNEL_SIZE, _CameraDepthTexture_TexelSize.xy); + } + + ENDCG + } + + // pass 1 - vertical blur (hires) + Pass + { + CGPROGRAM + #pragma vertex vert + #pragma fragment verticalFrag + #pragma target 4.0 + + fixed4 verticalFrag(v2f input) : SV_Target + { + return BilateralBlur(input, int2(0, 1), _CameraDepthTexture, sampler_CameraDepthTexture, FULL_RES_BLUR_KERNEL_SIZE, _CameraDepthTexture_TexelSize.xy); + } + + ENDCG + } + + // pass 2 - horizontal blur (lores) + Pass + { + CGPROGRAM + #pragma vertex vert + #pragma fragment horizontalFrag + #pragma target 4.0 + + fixed4 horizontalFrag(v2f input) : SV_Target + { + return BilateralBlur(input, int2(1, 0), _HalfResDepthBuffer, sampler_HalfResDepthBuffer, HALF_RES_BLUR_KERNEL_SIZE, _HalfResDepthBuffer_TexelSize.xy); + } + + ENDCG + } + + // pass 3 - vertical blur (lores) + Pass + { + CGPROGRAM + #pragma vertex vert + #pragma fragment verticalFrag + #pragma target 4.0 + + fixed4 verticalFrag(v2f input) : SV_Target + { + return BilateralBlur(input, int2(0, 1), _HalfResDepthBuffer, sampler_HalfResDepthBuffer, HALF_RES_BLUR_KERNEL_SIZE, _HalfResDepthBuffer_TexelSize.xy); + } + + ENDCG + } + + // pass 4 - downsample depth to half + Pass + { + CGPROGRAM + #pragma vertex vertHalfDepth + #pragma fragment frag + #pragma target gl4.1 + + v2fDownsample vertHalfDepth(appdata v) + { + return vertDownsampleDepth(v, _CameraDepthTexture_TexelSize); + } + + float frag(v2fDownsample input) : SV_Target + { + return DownsampleDepth(input, _CameraDepthTexture, sampler_CameraDepthTexture); + } + + ENDCG + } + + // pass 5 - bilateral upsample + Pass + { + Blend One Zero + + CGPROGRAM + #pragma vertex vertUpsampleToFull + #pragma fragment frag + #pragma target 4.0 + + v2fUpsample vertUpsampleToFull(appdata v) + { + return vertUpsample(v, _HalfResDepthBuffer_TexelSize); + } + float4 frag(v2fUpsample input) : SV_Target + { + return BilateralUpsample(input, _CameraDepthTexture, _HalfResDepthBuffer, _HalfResColor, sampler_HalfResColor, sampler_HalfResDepthBuffer); + } + + ENDCG + } + + // pass 6 - downsample depth to quarter + Pass + { + CGPROGRAM + #pragma vertex vertQuarterDepth + #pragma fragment frag + #pragma target gl4.1 + + v2fDownsample vertQuarterDepth(appdata v) + { + return vertDownsampleDepth(v, _HalfResDepthBuffer_TexelSize); + } + + float frag(v2fDownsample input) : SV_Target + { + return DownsampleDepth(input, _HalfResDepthBuffer, sampler_HalfResDepthBuffer); + } + + ENDCG + } + + // pass 7 - bilateral upsample quarter to full + Pass + { + Blend One Zero + + CGPROGRAM + #pragma vertex vertUpsampleToFull + #pragma fragment frag + #pragma target 4.0 + + v2fUpsample vertUpsampleToFull(appdata v) + { + return vertUpsample(v, _QuarterResDepthBuffer_TexelSize); + } + float4 frag(v2fUpsample input) : SV_Target + { + return BilateralUpsample(input, _CameraDepthTexture, _QuarterResDepthBuffer, _QuarterResColor, sampler_QuarterResColor, sampler_QuarterResDepthBuffer); + } + + ENDCG + } + + // pass 8 - horizontal blur (quarter res) + Pass + { + CGPROGRAM + #pragma vertex vert + #pragma fragment horizontalFrag + #pragma target 4.0 + + fixed4 horizontalFrag(v2f input) : SV_Target + { + return BilateralBlur(input, int2(1, 0), _QuarterResDepthBuffer, sampler_QuarterResDepthBuffer, QUARTER_RES_BLUR_KERNEL_SIZE, _QuarterResDepthBuffer_TexelSize.xy); + } + + ENDCG + } + + // pass 9 - vertical blur (quarter res) + Pass + { + CGPROGRAM + #pragma vertex vert + #pragma fragment verticalFrag + #pragma target 4.0 + + fixed4 verticalFrag(v2f input) : SV_Target + { + return BilateralBlur(input, int2(0, 1), _QuarterResDepthBuffer, sampler_QuarterResDepthBuffer, QUARTER_RES_BLUR_KERNEL_SIZE, _QuarterResDepthBuffer_TexelSize.xy); + } + + ENDCG + } + + // pass 10 - downsample depth to half (fallback for DX10) + Pass + { + CGPROGRAM + #pragma vertex vertHalfDepth + #pragma fragment frag + #pragma target 4.0 + + v2fDownsample vertHalfDepth(appdata v) + { + return vertDownsampleDepth(v, _CameraDepthTexture_TexelSize); + } + + float frag(v2fDownsample input) : SV_Target + { + return DownsampleDepth(input, _CameraDepthTexture, sampler_CameraDepthTexture); + } + + ENDCG + } + + // pass 11 - downsample depth to quarter (fallback for DX10) + Pass + { + CGPROGRAM + #pragma vertex vertQuarterDepth + #pragma fragment frag + #pragma target 4.0 + + v2fDownsample vertQuarterDepth(appdata v) + { + return vertDownsampleDepth(v, _HalfResDepthBuffer_TexelSize); + } + + float frag(v2fDownsample input) : SV_Target + { + return DownsampleDepth(input, _HalfResDepthBuffer, sampler_HalfResDepthBuffer); + } + + ENDCG + } + } +} diff --git a/tests/Fixtures/VolumetricLights/Imported/BlitAdd.shader b/tests/Fixtures/VolumetricLights/Imported/BlitAdd.shader new file mode 100644 index 0000000..77ead93 --- /dev/null +++ b/tests/Fixtures/VolumetricLights/Imported/BlitAdd.shader @@ -0,0 +1,92 @@ +// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)' + +// Copyright(c) 2016, Michal Skalsky +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors +// may be used to endorse or promote products derived from this software without +// specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +// OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.IN NO EVENT +// SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT +// OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +// TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +// EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + +Shader "Hidden/BlitAdd" +{ + Properties{ _MainTex("Texture", any) = "" {} } + SubShader + { + Pass + { + ZTest Always Cull Off ZWrite Off + Blend One Zero + + CGPROGRAM + #pragma vertex vert + #pragma fragment frag + + #include "UnityCG.cginc" + + sampler2D _MainTex; + sampler2D _Source; + uniform float4 _MainTex_ST; + float4 _Source_TexelSize; + + struct appdata_t + { + float4 vertex : POSITION; + float2 texcoord : TEXCOORD0; + }; + + struct v2f + { + float4 vertex : SV_POSITION; + float2 texcoord : TEXCOORD0; + }; + + v2f vert(appdata_t v) + { + v2f o; + o.vertex = UnityObjectToClipPos(v.vertex); + o.texcoord = TRANSFORM_TEX(v.texcoord.xy, _MainTex); + + return o; + } + + fixed4 frag(v2f i) : SV_Target + { + float4 main = tex2D(_MainTex, i.texcoord); + +#if UNITY_UV_STARTS_AT_TOP + if (_Source_TexelSize.y < 0) + i.texcoord.y = 1 - i.texcoord.y; +#endif + float4 source = tex2D(_Source, i.texcoord); + + source *= main.w; + source.xyz += main.xyz; + return source; + } + ENDCG + } + } + Fallback Off +} diff --git a/tests/Fixtures/VolumetricLights/Imported/VolumetricLight.shader b/tests/Fixtures/VolumetricLights/Imported/VolumetricLight.shader new file mode 100644 index 0000000..cc9f8a5 --- /dev/null +++ b/tests/Fixtures/VolumetricLights/Imported/VolumetricLight.shader @@ -0,0 +1,648 @@ +// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld' +// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)' +// Upgrade NOTE: replaced 'unity_World2Shadow' with 'unity_WorldToShadow' + +// Copyright(c) 2016, Michal Skalsky +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors +// may be used to endorse or promote products derived from this software without +// specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +// OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.IN NO EVENT +// SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT +// OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +// TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +// EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + +Shader "Sandbox/VolumetricLight" +{ + Properties + { + [HideInInspector]_MainTex ("Texture", 2D) = "white" {} + [HideInInspector]_ZTest ("ZTest", Float) = 0 + [HideInInspector]_LightColor("_LightColor", Color) = (1,1,1,1) + } + SubShader + { + Tags { "RenderType"="Opaque" } + LOD 100 + + CGINCLUDE + + #if defined(SHADOWS_DEPTH) || defined(SHADOWS_CUBE) + #define SHADOWS_NATIVE + #endif + + #include "UnityCG.cginc" + #include "UnityDeferredLibrary.cginc" + + sampler3D _NoiseTexture; + sampler2D _DitherTexture; + + float4 _FrustumCorners[4]; + + struct appdata + { + float4 vertex : POSITION; + }; + + float4x4 _WorldViewProj; + float4x4 _MyLightMatrix0; + float4x4 _MyWorld2Shadow; + + float3 _CameraForward; + + // x: scattering coef, y: extinction coef, z: range w: skybox extinction coef + float4 _VolumetricLight; + // x: 1 - g^2, y: 1 + g^2, z: 2*g, w: 1/4pi + float4 _MieG; + + // x: scale, y: intensity, z: intensity offset + float4 _NoiseData; + // x: x velocity, y: z velocity + float4 _NoiseVelocity; + // x: ground level, y: height scale, z: unused, w: unused + float4 _HeightFog; + //float4 _LightDir; + + float _MaxRayLength; + + int _SampleCount; + + struct v2f + { + float4 pos : SV_POSITION; + float4 uv : TEXCOORD0; + float3 wpos : TEXCOORD1; + }; + + v2f vert(appdata v) + { + v2f o; + o.pos = mul(_WorldViewProj, v.vertex); + o.uv = ComputeScreenPos(o.pos); + o.wpos = mul(unity_ObjectToWorld, v.vertex); + return o; + } + + //----------------------------------------------------------------------------------------- + // GetCascadeWeights_SplitSpheres + //----------------------------------------------------------------------------------------- + inline fixed4 GetCascadeWeights_SplitSpheres(float3 wpos) + { + float3 fromCenter0 = wpos.xyz - unity_ShadowSplitSpheres[0].xyz; + float3 fromCenter1 = wpos.xyz - unity_ShadowSplitSpheres[1].xyz; + float3 fromCenter2 = wpos.xyz - unity_ShadowSplitSpheres[2].xyz; + float3 fromCenter3 = wpos.xyz - unity_ShadowSplitSpheres[3].xyz; + float4 distances2 = float4(dot(fromCenter0, fromCenter0), dot(fromCenter1, fromCenter1), dot(fromCenter2, fromCenter2), dot(fromCenter3, fromCenter3)); + + fixed4 weights = float4(distances2 < unity_ShadowSplitSqRadii); + weights.yzw = saturate(weights.yzw - weights.xyz); + return weights; + } + + //----------------------------------------------------------------------------------------- + // GetCascadeShadowCoord + //----------------------------------------------------------------------------------------- + inline float4 GetCascadeShadowCoord(float4 wpos, fixed4 cascadeWeights) + { + float3 sc0 = mul(unity_WorldToShadow[0], wpos).xyz; + float3 sc1 = mul(unity_WorldToShadow[1], wpos).xyz; + float3 sc2 = mul(unity_WorldToShadow[2], wpos).xyz; + float3 sc3 = mul(unity_WorldToShadow[3], wpos).xyz; + + float4 shadowMapCoordinate = float4(sc0 * cascadeWeights[0] + sc1 * cascadeWeights[1] + sc2 * cascadeWeights[2] + sc3 * cascadeWeights[3], 1); +#if defined(UNITY_REVERSED_Z) + float noCascadeWeights = 1 - dot(cascadeWeights, float4(1, 1, 1, 1)); + shadowMapCoordinate.z += noCascadeWeights; +#endif + return shadowMapCoordinate; + } + + UNITY_DECLARE_SHADOWMAP(_CascadeShadowMapTexture); + + //----------------------------------------------------------------------------------------- + // GetLightAttenuation + //----------------------------------------------------------------------------------------- + float GetLightAttenuation(float3 wpos) + { + float atten = 0; +#if defined (DIRECTIONAL) || defined (DIRECTIONAL_COOKIE) + atten = 1; +#if defined (SHADOWS_DEPTH) + // sample cascade shadow map + float4 cascadeWeights = GetCascadeWeights_SplitSpheres(wpos); + bool inside = dot(cascadeWeights, float4(1, 1, 1, 1)) < 4; + float4 samplePos = GetCascadeShadowCoord(float4(wpos, 1), cascadeWeights); + + atten = inside ? UNITY_SAMPLE_SHADOW(_CascadeShadowMapTexture, samplePos.xyz) : 1.0f; + atten = _LightShadowData.r + atten * (1 - _LightShadowData.r); + //atten = inside ? tex2Dproj(_ShadowMapTexture, float4((samplePos).xyz, 1)).r : 1.0f; +#endif +#if defined (DIRECTIONAL_COOKIE) + // NOT IMPLEMENTED +#endif +#elif defined (SPOT) + float3 tolight = _LightPos.xyz - wpos; + half3 lightDir = normalize(tolight); + + float4 uvCookie = mul(_MyLightMatrix0, float4(wpos, 1)); + // negative bias because http://aras-p.info/blog/2010/01/07/screenspace-vs-mip-mapping/ + atten = tex2Dbias(_LightTexture0, float4(uvCookie.xy / uvCookie.w, 0, -8)).w; + atten *= uvCookie.w < 0; + float att = dot(tolight, tolight) * _LightPos.w; + atten *= tex2D(_LightTextureB0, att.rr).UNITY_ATTEN_CHANNEL; + +#if defined(SHADOWS_DEPTH) + float4 shadowCoord = mul(_MyWorld2Shadow, float4(wpos, 1)); + atten *= saturate(UnitySampleShadowmap(shadowCoord)); +#endif +#elif defined (POINT) || defined (POINT_COOKIE) + float3 tolight = wpos - _LightPos.xyz; + half3 lightDir = -normalize(tolight); + + float att = dot(tolight, tolight) * _LightPos.w; + atten = tex2D(_LightTextureB0, att.rr).UNITY_ATTEN_CHANNEL; + + atten *= UnityDeferredComputeShadow(tolight, 0, float2(0, 0)); + +#if defined (POINT_COOKIE) + atten *= texCUBEbias(_LightTexture0, float4(mul(_MyLightMatrix0, half4(wpos, 1)).xyz, -8)).w; +#endif //POINT_COOKIE +#endif + return atten; + } + + //----------------------------------------------------------------------------------------- + // ApplyHeightFog + //----------------------------------------------------------------------------------------- + void ApplyHeightFog(float3 wpos, inout float density) + { +#ifdef HEIGHT_FOG + density *= exp(-(wpos.y + _HeightFog.x) * _HeightFog.y); +#endif + } + + //----------------------------------------------------------------------------------------- + // GetDensity + //----------------------------------------------------------------------------------------- + float GetDensity(float3 wpos) + { + float density = 1; +#ifdef NOISE + float noise = tex3D(_NoiseTexture, frac(wpos * _NoiseData.x + float3(_Time.y * _NoiseVelocity.x, 0, _Time.y * _NoiseVelocity.y))); + noise = saturate(noise - _NoiseData.z) * _NoiseData.y; + density = saturate(noise); +#endif + ApplyHeightFog(wpos, density); + + return density; + } + + //----------------------------------------------------------------------------------------- + // MieScattering + //----------------------------------------------------------------------------------------- + float MieScattering(float cosAngle, float4 g) + { + return g.w * (g.x / (pow(g.y - g.z * cosAngle, 1.5))); + } + + //----------------------------------------------------------------------------------------- + // RayMarch + //----------------------------------------------------------------------------------------- + float4 RayMarch(float2 screenPos, float3 rayStart, float3 rayDir, float rayLength) + { +#ifdef DITHER_4_4 + float2 interleavedPos = (fmod(floor(screenPos.xy), 4.0)); + float offset = tex2D(_DitherTexture, interleavedPos / 4.0 + float2(0.5 / 4.0, 0.5 / 4.0)).w; +#else + float2 interleavedPos = (fmod(floor(screenPos.xy), 8.0)); + float offset = tex2D(_DitherTexture, interleavedPos / 8.0 + float2(0.5 / 8.0, 0.5 / 8.0)).w; +#endif + + int stepCount = _SampleCount; + + float stepSize = rayLength / stepCount; + float3 step = rayDir * stepSize; + + float3 currentPosition = rayStart + step * offset; + + float4 vlight = 0; + + float cosAngle; +#if defined (DIRECTIONAL) || defined (DIRECTIONAL_COOKIE) + float extinction = 0; + cosAngle = dot(_LightDir.xyz, -rayDir); +#else + // we don't know about density between camera and light's volume, assume 0.5 + float extinction = length(_WorldSpaceCameraPos - currentPosition) * _VolumetricLight.y * 0.5; +#endif + [loop] + for (int i = 0; i < stepCount; ++i) + { + float atten = GetLightAttenuation(currentPosition); + float density = GetDensity(currentPosition); + + float scattering = _VolumetricLight.x * stepSize * density; + extinction += _VolumetricLight.y * stepSize * density;// +scattering; + + float4 light = atten * scattering * exp(-extinction); + +//#if PHASE_FUNCTOIN +#if !defined (DIRECTIONAL) && !defined (DIRECTIONAL_COOKIE) + // phase functino for spot and point lights + float3 tolight = normalize(currentPosition - _LightPos.xyz); + cosAngle = dot(tolight, -rayDir); + light *= MieScattering(cosAngle, _MieG); +#endif +//#endif + vlight += light; + + currentPosition += step; + } + +#if defined (DIRECTIONAL) || defined (DIRECTIONAL_COOKIE) + // apply phase function for dir light + vlight *= MieScattering(cosAngle, _MieG); +#endif + + // apply light's color + vlight *= _LightColor; + + vlight = max(0, vlight); +#if defined (DIRECTIONAL) || defined (DIRECTIONAL_COOKIE) // use "proper" out-scattering/absorption for dir light + vlight.w = exp(-extinction); +#else + vlight.w = 0; +#endif + return vlight; + } + + //----------------------------------------------------------------------------------------- + // RayConeIntersect + //----------------------------------------------------------------------------------------- + float2 RayConeIntersect(in float3 f3ConeApex, in float3 f3ConeAxis, in float fCosAngle, in float3 f3RayStart, in float3 f3RayDir) + { + float inf = 10000; + f3RayStart -= f3ConeApex; + float a = dot(f3RayDir, f3ConeAxis); + float b = dot(f3RayDir, f3RayDir); + float c = dot(f3RayStart, f3ConeAxis); + float d = dot(f3RayStart, f3RayDir); + float e = dot(f3RayStart, f3RayStart); + fCosAngle *= fCosAngle; + float A = a*a - b*fCosAngle; + float B = 2 * (c*a - d*fCosAngle); + float C = c*c - e*fCosAngle; + float D = B*B - 4 * A*C; + + if (D > 0) + { + D = sqrt(D); + float2 t = (-B + sign(A)*float2(-D, +D)) / (2 * A); + bool2 b2IsCorrect = c + a * t > 0 && t > 0; + t = t * b2IsCorrect + !b2IsCorrect * (inf); + return t; + } + else // no intersection + return inf; + } + + //----------------------------------------------------------------------------------------- + // RayPlaneIntersect + //----------------------------------------------------------------------------------------- + float RayPlaneIntersect(in float3 planeNormal, in float planeD, in float3 rayOrigin, in float3 rayDir) + { + float NdotD = dot(planeNormal, rayDir); + float NdotO = dot(planeNormal, rayOrigin); + + float t = -(NdotO + planeD) / NdotD; + if (t < 0) + t = 100000; + return t; + } + + ENDCG + + // pass 0 - point light, camera inside + Pass + { + ZTest Off + Cull Front + ZWrite Off + Blend One One + + CGPROGRAM + #pragma vertex vert + #pragma fragment fragPointInside + #pragma target 4.0 + + #define UNITY_HDR_ON + + #pragma shader_feature HEIGHT_FOG + #pragma shader_feature NOISE + #pragma shader_feature SHADOWS_CUBE + #pragma shader_feature POINT_COOKIE + #pragma shader_feature POINT + + #ifdef SHADOWS_DEPTH + #define SHADOWS_NATIVE + #endif + + + fixed4 fragPointInside(v2f i) : SV_Target + { + float2 uv = i.uv.xy / i.uv.w; + + // read depth and reconstruct world position + float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv); + + float3 rayStart = _WorldSpaceCameraPos; + float3 rayEnd = i.wpos; + + float3 rayDir = (rayEnd - rayStart); + float rayLength = length(rayDir); + + rayDir /= rayLength; + + float linearDepth = LinearEyeDepth(depth); + float projectedDepth = linearDepth / dot(_CameraForward, rayDir); + rayLength = min(rayLength, projectedDepth); + + return RayMarch(i.pos.xy, rayStart, rayDir, rayLength); + } + ENDCG + } + + // pass 1 - spot light, camera inside + Pass + { + ZTest Off + Cull Front + ZWrite Off + Blend One One + + CGPROGRAM +#pragma vertex vert +#pragma fragment fragPointInside +#pragma target 4.0 + +#define UNITY_HDR_ON + +#pragma shader_feature HEIGHT_FOG +#pragma shader_feature NOISE +#pragma shader_feature SHADOWS_DEPTH +#pragma shader_feature SPOT + +#ifdef SHADOWS_DEPTH +#define SHADOWS_NATIVE +#endif + + fixed4 fragPointInside(v2f i) : SV_Target + { + float2 uv = i.uv.xy / i.uv.w; + + // read depth and reconstruct world position + float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv); + + float3 rayStart = _WorldSpaceCameraPos; + float3 rayEnd = i.wpos; + + float3 rayDir = (rayEnd - rayStart); + float rayLength = length(rayDir); + + rayDir /= rayLength; + + float linearDepth = LinearEyeDepth(depth); + float projectedDepth = linearDepth / dot(_CameraForward, rayDir); + rayLength = min(rayLength, projectedDepth); + + return RayMarch(i.pos.xy, rayStart, rayDir, rayLength); + } + ENDCG + } + + // pass 2 - point light, camera outside + Pass + { + //ZTest Off + ZTest [_ZTest] + Cull Back + ZWrite Off + Blend One One + + CGPROGRAM + #pragma vertex vert + #pragma fragment fragPointOutside + #pragma target 4.0 + + #define UNITY_HDR_ON + + #pragma shader_feature HEIGHT_FOG + #pragma shader_feature SHADOWS_CUBE + #pragma shader_feature NOISE + //#pragma multi_compile POINT POINT_COOKIE + #pragma shader_feature POINT_COOKIE + #pragma shader_feature POINT + + fixed4 fragPointOutside(v2f i) : SV_Target + { + float2 uv = i.uv.xy / i.uv.w; + + // read depth and reconstruct world position + float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv); + + float3 rayStart = _WorldSpaceCameraPos; + float3 rayEnd = i.wpos; + + float3 rayDir = (rayEnd - rayStart); + float rayLength = length(rayDir); + + rayDir /= rayLength; + + float3 lightToCamera = _WorldSpaceCameraPos - _LightPos; + + float b = dot(rayDir, lightToCamera); + float c = dot(lightToCamera, lightToCamera) - (_VolumetricLight.z * _VolumetricLight.z); + + float d = sqrt((b*b) - c); + float start = -b - d; + float end = -b + d; + + float linearDepth = LinearEyeDepth(depth); + float projectedDepth = linearDepth / dot(_CameraForward, rayDir); + end = min(end, projectedDepth); + + rayStart = rayStart + rayDir * start; + rayLength = end - start; + + return RayMarch(i.pos.xy, rayStart, rayDir, rayLength); + } + ENDCG + } + + // pass 3 - spot light, camera outside + Pass + { + //ZTest Off + ZTest[_ZTest] + Cull Back + ZWrite Off + Blend One One + + CGPROGRAM + #pragma vertex vert + #pragma fragment fragSpotOutside + #pragma target 4.0 + + #define UNITY_HDR_ON + + #pragma shader_feature HEIGHT_FOG + #pragma shader_feature SHADOWS_DEPTH + #pragma shader_feature NOISE + #pragma shader_feature SPOT + + #ifdef SHADOWS_DEPTH + #define SHADOWS_NATIVE + #endif + + float _CosAngle; + float4 _ConeAxis; + float4 _ConeApex; + float _PlaneD; + + fixed4 fragSpotOutside(v2f i) : SV_Target + { + float2 uv = i.uv.xy / i.uv.w; + + // read depth and reconstruct world position + float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv); + + float3 rayStart = _WorldSpaceCameraPos; + float3 rayEnd = i.wpos; + + float3 rayDir = (rayEnd - rayStart); + float rayLength = length(rayDir); + + rayDir /= rayLength; + + + // inside cone + float3 r1 = rayEnd + rayDir * 0.001; + + // plane intersection + float planeCoord = RayPlaneIntersect(_ConeAxis, _PlaneD, r1, rayDir); + // ray cone intersection + float2 lineCoords = RayConeIntersect(_ConeApex, _ConeAxis, _CosAngle, r1, rayDir); + + float linearDepth = LinearEyeDepth(depth); + float projectedDepth = linearDepth / dot(_CameraForward, rayDir); + + float z = (projectedDepth - rayLength); + rayLength = min(planeCoord, min(lineCoords.x, lineCoords.y)); + rayLength = min(rayLength, z); + + return RayMarch(i.pos.xy, rayEnd, rayDir, rayLength); + } + ENDCG + } + + // pass 4 - directional light + Pass + { + ZTest Off + Cull Off + ZWrite Off + Blend One One, One Zero + + CGPROGRAM + + #pragma vertex vertDir + #pragma fragment fragDir + #pragma target 4.0 + + #define UNITY_HDR_ON + + #pragma shader_feature HEIGHT_FOG + #pragma shader_feature NOISE + #pragma shader_feature SHADOWS_DEPTH + #pragma shader_feature DIRECTIONAL_COOKIE + #pragma shader_feature DIRECTIONAL + + #ifdef SHADOWS_DEPTH + #define SHADOWS_NATIVE + #endif + + struct VSInput + { + float4 vertex : POSITION; + float2 uv : TEXCOORD0; + uint vertexId : SV_VertexID; + }; + + struct PSInput + { + float4 pos : SV_POSITION; + float2 uv : TEXCOORD0; + float3 wpos : TEXCOORD1; + }; + + PSInput vertDir(VSInput i) + { + PSInput o; + + o.pos = UnityObjectToClipPos(i.vertex); + o.uv = i.uv; + + // SV_VertexId doesn't work on OpenGL for some reason -> reconstruct id from uv + //o.wpos = _FrustumCorners[i.vertexId]; + o.wpos = _FrustumCorners[i.uv.x + i.uv.y*2]; + + return o; + } + + fixed4 fragDir(PSInput i) : SV_Target + { + float2 uv = i.uv.xy; + float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv); + float linearDepth = Linear01Depth(depth); + + float3 wpos = i.wpos; + float3 rayStart = _WorldSpaceCameraPos; + float3 rayDir = wpos - _WorldSpaceCameraPos; + rayDir *= linearDepth; + + float rayLength = length(rayDir); + rayDir /= rayLength; + + rayLength = min(rayLength, _MaxRayLength); + + float4 color = RayMarch(i.pos.xy, rayStart, rayDir, rayLength); + + if (linearDepth > 0.999999) + { + color.w = lerp(color.w, 1, _VolumetricLight.w); + } + return color; + } + ENDCG + } + } +} diff --git a/tests/Fixtures/VolumetricLights/Json/BilateralBlur.json b/tests/Fixtures/VolumetricLights/Json/BilateralBlur.json new file mode 100644 index 0000000..0d120d7 --- /dev/null +++ b/tests/Fixtures/VolumetricLights/Json/BilateralBlur.json @@ -0,0 +1,7930 @@ +{ + "m_Name": "", + "m_ParsedForm": { + "m_PropInfo": { + "m_Props": [ + { + "m_Name": "_MainTex", + "m_Description": "Texture", + "m_Attributes": [], + "m_Type": 4, + "m_Flags": 0, + "m_DefValue[0]": 0.0, + "m_DefValue[1]": 0.0, + "m_DefValue[2]": 0.0, + "m_DefValue[3]": 0.0, + "m_DefTexture": { + "m_DefaultName": "", + "m_TexDim": 1 + } + } + ] + }, + "m_SubShaders": [ + { + "m_Passes": [ + { + "m_NameIndices": [ + { + "Key": "UnityPerCamera", + "Value": 6 + }, + { + "Key": "UnityPerDraw", + "Value": 0 + }, + { + "Key": "UnityPerFrame", + "Value": 1 + }, + { + "Key": "_CameraDepthTexture", + "Value": 4 + }, + { + "Key": "_MainTex", + "Value": 5 + }, + { + "Key": "_ZBufferParams", + "Value": 7 + }, + { + "Key": "unity_MatrixVP", + "Value": 3 + }, + { + "Key": "unity_ObjectToWorld", + "Value": 2 + } + ], + "m_Type": 0, + "m_State": { + "m_Name": "", + "rtBlend0": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend1": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend2": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend3": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend4": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend5": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend6": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend7": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtSeparateBlend": false, + "zClip": { + "val": 1.0, + "name": "" + }, + "zTest": { + "val": 8.0, + "name": "" + }, + "zWrite": { + "val": 0.0, + "name": "" + }, + "culling": { + "val": 0.0, + "name": "" + }, + "conservative": { + "val": 0.0, + "name": "" + }, + "offsetFactor": { + "val": 0.0, + "name": "" + }, + "offsetUnits": { + "val": 0.0, + "name": "" + }, + "alphaToMask": { + "val": 0.0, + "name": "" + }, + "stencilOp": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilOpFront": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilOpBack": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilReadMask": { + "val": 255.0, + "name": "" + }, + "stencilWriteMask": { + "val": 255.0, + "name": "" + }, + "stencilRef": { + "val": 0.0, + "name": "" + }, + "fogStart": { + "val": 0.0, + "name": "unity_FogStart" + }, + "fogEnd": { + "val": 0.0, + "name": "unity_FogEnd" + }, + "fogDensity": { + "val": 0.0, + "name": "unity_FogDensity" + }, + "fogColor": { + "x": { + "val": 0.0, + "name": "" + }, + "y": { + "val": 0.0, + "name": "" + }, + "z": { + "val": 0.0, + "name": "" + }, + "w": { + "val": 0.0, + "name": "" + }, + "name": "unity_FogColor" + }, + "fogMode": -1, + "gpuProgramID": 24359, + "m_Tags": { + "tags": [] + }, + "m_LOD": 0, + "lighting": false + }, + "m_ProgramMask": 6, + "progVertex": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [ + [], + [], + [], + [ + { + "m_BlobIndex": 1, + "m_KeywordIndices": [], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + } + ] + ], + "m_ParameterBlobIndices": [ + [], + [], + [], + [ + 0 + ] + ], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [ + { + "m_NameIndex": 0, + "m_MatrixParams": [ + { + "m_NameIndex": 2, + "m_Index": 0, + "m_ArraySize": 0, + "m_Type": 0, + "m_RowCount": 4 + } + ], + "m_VectorParams": [], + "m_StructParams": [], + "m_Size": 176, + "m_IsPartialCB": true + }, + { + "m_NameIndex": 1, + "m_MatrixParams": [ + { + "m_NameIndex": 3, + "m_Index": 272, + "m_ArraySize": 0, + "m_Type": 0, + "m_RowCount": 4 + } + ], + "m_VectorParams": [], + "m_StructParams": [], + "m_Size": 368, + "m_IsPartialCB": true + } + ], + "m_ConstantBufferBindings": [ + { + "m_NameIndex": 0, + "m_Index": 0, + "m_ArraySize": 1 + }, + { + "m_NameIndex": 1, + "m_Index": 1, + "m_ArraySize": 1 + } + ], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [ + 0, + 1, + 2, + 3 + ] + }, + "progFragment": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [ + [], + [], + [], + [ + { + "m_BlobIndex": 3, + "m_KeywordIndices": [], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + } + ] + ], + "m_ParameterBlobIndices": [ + [], + [], + [], + [ + 2 + ] + ], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [ + { + "m_NameIndex": 4, + "m_Index": 0, + "m_SamplerIndex": 0, + "m_MultiSampled": false, + "m_Dim": 2 + }, + { + "m_NameIndex": 5, + "m_Index": 1, + "m_SamplerIndex": 1, + "m_MultiSampled": false, + "m_Dim": 2 + } + ], + "m_BufferParams": [], + "m_ConstantBuffers": [ + { + "m_NameIndex": 6, + "m_MatrixParams": [], + "m_VectorParams": [ + { + "m_NameIndex": 7, + "m_Index": 112, + "m_ArraySize": 0, + "m_Type": 0, + "m_Dim": 4 + } + ], + "m_StructParams": [], + "m_Size": 144, + "m_IsPartialCB": true + } + ], + "m_ConstantBufferBindings": [ + { + "m_NameIndex": 6, + "m_Index": 0, + "m_ArraySize": 1 + } + ], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [ + 0, + 1, + 2, + 3 + ] + }, + "progGeometry": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progHull": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progDomain": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progRayTracing": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "m_HasInstancingVariant": false, + "m_HasProceduralInstancingVariant": false, + "m_UseName": "", + "m_Name": "", + "m_TextureName": "", + "m_Tags": { + "tags": [] + } + }, + { + "m_NameIndices": [ + { + "Key": "UnityPerCamera", + "Value": 6 + }, + { + "Key": "UnityPerDraw", + "Value": 0 + }, + { + "Key": "UnityPerFrame", + "Value": 1 + }, + { + "Key": "_CameraDepthTexture", + "Value": 4 + }, + { + "Key": "_MainTex", + "Value": 5 + }, + { + "Key": "_ZBufferParams", + "Value": 7 + }, + { + "Key": "unity_MatrixVP", + "Value": 3 + }, + { + "Key": "unity_ObjectToWorld", + "Value": 2 + } + ], + "m_Type": 0, + "m_State": { + "m_Name": "", + "rtBlend0": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend1": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend2": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend3": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend4": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend5": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend6": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend7": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtSeparateBlend": false, + "zClip": { + "val": 1.0, + "name": "" + }, + "zTest": { + "val": 8.0, + "name": "" + }, + "zWrite": { + "val": 0.0, + "name": "" + }, + "culling": { + "val": 0.0, + "name": "" + }, + "conservative": { + "val": 0.0, + "name": "" + }, + "offsetFactor": { + "val": 0.0, + "name": "" + }, + "offsetUnits": { + "val": 0.0, + "name": "" + }, + "alphaToMask": { + "val": 0.0, + "name": "" + }, + "stencilOp": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilOpFront": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilOpBack": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilReadMask": { + "val": 255.0, + "name": "" + }, + "stencilWriteMask": { + "val": 255.0, + "name": "" + }, + "stencilRef": { + "val": 0.0, + "name": "" + }, + "fogStart": { + "val": 0.0, + "name": "unity_FogStart" + }, + "fogEnd": { + "val": 0.0, + "name": "unity_FogEnd" + }, + "fogDensity": { + "val": 0.0, + "name": "unity_FogDensity" + }, + "fogColor": { + "x": { + "val": 0.0, + "name": "" + }, + "y": { + "val": 0.0, + "name": "" + }, + "z": { + "val": 0.0, + "name": "" + }, + "w": { + "val": 0.0, + "name": "" + }, + "name": "unity_FogColor" + }, + "fogMode": -1, + "gpuProgramID": 83169, + "m_Tags": { + "tags": [] + }, + "m_LOD": 0, + "lighting": false + }, + "m_ProgramMask": 6, + "progVertex": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [ + [], + [], + [], + [ + { + "m_BlobIndex": 1, + "m_KeywordIndices": [], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + } + ] + ], + "m_ParameterBlobIndices": [ + [], + [], + [], + [ + 0 + ] + ], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [ + { + "m_NameIndex": 0, + "m_MatrixParams": [ + { + "m_NameIndex": 2, + "m_Index": 0, + "m_ArraySize": 0, + "m_Type": 0, + "m_RowCount": 4 + } + ], + "m_VectorParams": [], + "m_StructParams": [], + "m_Size": 176, + "m_IsPartialCB": true + }, + { + "m_NameIndex": 1, + "m_MatrixParams": [ + { + "m_NameIndex": 3, + "m_Index": 272, + "m_ArraySize": 0, + "m_Type": 0, + "m_RowCount": 4 + } + ], + "m_VectorParams": [], + "m_StructParams": [], + "m_Size": 368, + "m_IsPartialCB": true + } + ], + "m_ConstantBufferBindings": [ + { + "m_NameIndex": 0, + "m_Index": 0, + "m_ArraySize": 1 + }, + { + "m_NameIndex": 1, + "m_Index": 1, + "m_ArraySize": 1 + } + ], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [ + 0, + 1, + 2, + 3 + ] + }, + "progFragment": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [ + [], + [], + [], + [ + { + "m_BlobIndex": 4, + "m_KeywordIndices": [], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + } + ] + ], + "m_ParameterBlobIndices": [ + [], + [], + [], + [ + 2 + ] + ], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [ + { + "m_NameIndex": 4, + "m_Index": 0, + "m_SamplerIndex": 0, + "m_MultiSampled": false, + "m_Dim": 2 + }, + { + "m_NameIndex": 5, + "m_Index": 1, + "m_SamplerIndex": 1, + "m_MultiSampled": false, + "m_Dim": 2 + } + ], + "m_BufferParams": [], + "m_ConstantBuffers": [ + { + "m_NameIndex": 6, + "m_MatrixParams": [], + "m_VectorParams": [ + { + "m_NameIndex": 7, + "m_Index": 112, + "m_ArraySize": 0, + "m_Type": 0, + "m_Dim": 4 + } + ], + "m_StructParams": [], + "m_Size": 144, + "m_IsPartialCB": true + } + ], + "m_ConstantBufferBindings": [ + { + "m_NameIndex": 6, + "m_Index": 0, + "m_ArraySize": 1 + } + ], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [ + 0, + 1, + 2, + 3 + ] + }, + "progGeometry": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progHull": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progDomain": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progRayTracing": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "m_HasInstancingVariant": false, + "m_HasProceduralInstancingVariant": false, + "m_UseName": "", + "m_Name": "", + "m_TextureName": "", + "m_Tags": { + "tags": [] + } + }, + { + "m_NameIndices": [ + { + "Key": "UnityPerCamera", + "Value": 6 + }, + { + "Key": "UnityPerDraw", + "Value": 0 + }, + { + "Key": "UnityPerFrame", + "Value": 1 + }, + { + "Key": "_HalfResDepthBuffer", + "Value": 4 + }, + { + "Key": "_MainTex", + "Value": 5 + }, + { + "Key": "_ZBufferParams", + "Value": 7 + }, + { + "Key": "unity_MatrixVP", + "Value": 3 + }, + { + "Key": "unity_ObjectToWorld", + "Value": 2 + } + ], + "m_Type": 0, + "m_State": { + "m_Name": "", + "rtBlend0": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend1": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend2": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend3": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend4": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend5": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend6": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend7": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtSeparateBlend": false, + "zClip": { + "val": 1.0, + "name": "" + }, + "zTest": { + "val": 8.0, + "name": "" + }, + "zWrite": { + "val": 0.0, + "name": "" + }, + "culling": { + "val": 0.0, + "name": "" + }, + "conservative": { + "val": 0.0, + "name": "" + }, + "offsetFactor": { + "val": 0.0, + "name": "" + }, + "offsetUnits": { + "val": 0.0, + "name": "" + }, + "alphaToMask": { + "val": 0.0, + "name": "" + }, + "stencilOp": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilOpFront": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilOpBack": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilReadMask": { + "val": 255.0, + "name": "" + }, + "stencilWriteMask": { + "val": 255.0, + "name": "" + }, + "stencilRef": { + "val": 0.0, + "name": "" + }, + "fogStart": { + "val": 0.0, + "name": "unity_FogStart" + }, + "fogEnd": { + "val": 0.0, + "name": "unity_FogEnd" + }, + "fogDensity": { + "val": 0.0, + "name": "unity_FogDensity" + }, + "fogColor": { + "x": { + "val": 0.0, + "name": "" + }, + "y": { + "val": 0.0, + "name": "" + }, + "z": { + "val": 0.0, + "name": "" + }, + "w": { + "val": 0.0, + "name": "" + }, + "name": "unity_FogColor" + }, + "fogMode": -1, + "gpuProgramID": 146473, + "m_Tags": { + "tags": [] + }, + "m_LOD": 0, + "lighting": false + }, + "m_ProgramMask": 6, + "progVertex": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [ + [], + [], + [], + [ + { + "m_BlobIndex": 1, + "m_KeywordIndices": [], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + } + ] + ], + "m_ParameterBlobIndices": [ + [], + [], + [], + [ + 0 + ] + ], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [ + { + "m_NameIndex": 0, + "m_MatrixParams": [ + { + "m_NameIndex": 2, + "m_Index": 0, + "m_ArraySize": 0, + "m_Type": 0, + "m_RowCount": 4 + } + ], + "m_VectorParams": [], + "m_StructParams": [], + "m_Size": 176, + "m_IsPartialCB": true + }, + { + "m_NameIndex": 1, + "m_MatrixParams": [ + { + "m_NameIndex": 3, + "m_Index": 272, + "m_ArraySize": 0, + "m_Type": 0, + "m_RowCount": 4 + } + ], + "m_VectorParams": [], + "m_StructParams": [], + "m_Size": 368, + "m_IsPartialCB": true + } + ], + "m_ConstantBufferBindings": [ + { + "m_NameIndex": 0, + "m_Index": 0, + "m_ArraySize": 1 + }, + { + "m_NameIndex": 1, + "m_Index": 1, + "m_ArraySize": 1 + } + ], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [ + 0, + 1, + 2, + 3 + ] + }, + "progFragment": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [ + [], + [], + [], + [ + { + "m_BlobIndex": 5, + "m_KeywordIndices": [], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + } + ] + ], + "m_ParameterBlobIndices": [ + [], + [], + [], + [ + 2 + ] + ], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [ + { + "m_NameIndex": 4, + "m_Index": 0, + "m_SamplerIndex": 0, + "m_MultiSampled": false, + "m_Dim": 2 + }, + { + "m_NameIndex": 5, + "m_Index": 1, + "m_SamplerIndex": 1, + "m_MultiSampled": false, + "m_Dim": 2 + } + ], + "m_BufferParams": [], + "m_ConstantBuffers": [ + { + "m_NameIndex": 6, + "m_MatrixParams": [], + "m_VectorParams": [ + { + "m_NameIndex": 7, + "m_Index": 112, + "m_ArraySize": 0, + "m_Type": 0, + "m_Dim": 4 + } + ], + "m_StructParams": [], + "m_Size": 144, + "m_IsPartialCB": true + } + ], + "m_ConstantBufferBindings": [ + { + "m_NameIndex": 6, + "m_Index": 0, + "m_ArraySize": 1 + } + ], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [ + 0, + 1, + 2, + 3 + ] + }, + "progGeometry": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progHull": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progDomain": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progRayTracing": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "m_HasInstancingVariant": false, + "m_HasProceduralInstancingVariant": false, + "m_UseName": "", + "m_Name": "", + "m_TextureName": "", + "m_Tags": { + "tags": [] + } + }, + { + "m_NameIndices": [ + { + "Key": "UnityPerCamera", + "Value": 6 + }, + { + "Key": "UnityPerDraw", + "Value": 0 + }, + { + "Key": "UnityPerFrame", + "Value": 1 + }, + { + "Key": "_HalfResDepthBuffer", + "Value": 4 + }, + { + "Key": "_MainTex", + "Value": 5 + }, + { + "Key": "_ZBufferParams", + "Value": 7 + }, + { + "Key": "unity_MatrixVP", + "Value": 3 + }, + { + "Key": "unity_ObjectToWorld", + "Value": 2 + } + ], + "m_Type": 0, + "m_State": { + "m_Name": "", + "rtBlend0": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend1": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend2": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend3": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend4": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend5": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend6": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend7": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtSeparateBlend": false, + "zClip": { + "val": 1.0, + "name": "" + }, + "zTest": { + "val": 8.0, + "name": "" + }, + "zWrite": { + "val": 0.0, + "name": "" + }, + "culling": { + "val": 0.0, + "name": "" + }, + "conservative": { + "val": 0.0, + "name": "" + }, + "offsetFactor": { + "val": 0.0, + "name": "" + }, + "offsetUnits": { + "val": 0.0, + "name": "" + }, + "alphaToMask": { + "val": 0.0, + "name": "" + }, + "stencilOp": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilOpFront": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilOpBack": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilReadMask": { + "val": 255.0, + "name": "" + }, + "stencilWriteMask": { + "val": 255.0, + "name": "" + }, + "stencilRef": { + "val": 0.0, + "name": "" + }, + "fogStart": { + "val": 0.0, + "name": "unity_FogStart" + }, + "fogEnd": { + "val": 0.0, + "name": "unity_FogEnd" + }, + "fogDensity": { + "val": 0.0, + "name": "unity_FogDensity" + }, + "fogColor": { + "x": { + "val": 0.0, + "name": "" + }, + "y": { + "val": 0.0, + "name": "" + }, + "z": { + "val": 0.0, + "name": "" + }, + "w": { + "val": 0.0, + "name": "" + }, + "name": "unity_FogColor" + }, + "fogMode": -1, + "gpuProgramID": 240513, + "m_Tags": { + "tags": [] + }, + "m_LOD": 0, + "lighting": false + }, + "m_ProgramMask": 6, + "progVertex": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [ + [], + [], + [], + [ + { + "m_BlobIndex": 1, + "m_KeywordIndices": [], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + } + ] + ], + "m_ParameterBlobIndices": [ + [], + [], + [], + [ + 0 + ] + ], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [ + { + "m_NameIndex": 0, + "m_MatrixParams": [ + { + "m_NameIndex": 2, + "m_Index": 0, + "m_ArraySize": 0, + "m_Type": 0, + "m_RowCount": 4 + } + ], + "m_VectorParams": [], + "m_StructParams": [], + "m_Size": 176, + "m_IsPartialCB": true + }, + { + "m_NameIndex": 1, + "m_MatrixParams": [ + { + "m_NameIndex": 3, + "m_Index": 272, + "m_ArraySize": 0, + "m_Type": 0, + "m_RowCount": 4 + } + ], + "m_VectorParams": [], + "m_StructParams": [], + "m_Size": 368, + "m_IsPartialCB": true + } + ], + "m_ConstantBufferBindings": [ + { + "m_NameIndex": 0, + "m_Index": 0, + "m_ArraySize": 1 + }, + { + "m_NameIndex": 1, + "m_Index": 1, + "m_ArraySize": 1 + } + ], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [ + 0, + 1, + 2, + 3 + ] + }, + "progFragment": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [ + [], + [], + [], + [ + { + "m_BlobIndex": 6, + "m_KeywordIndices": [], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + } + ] + ], + "m_ParameterBlobIndices": [ + [], + [], + [], + [ + 2 + ] + ], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [ + { + "m_NameIndex": 4, + "m_Index": 0, + "m_SamplerIndex": 0, + "m_MultiSampled": false, + "m_Dim": 2 + }, + { + "m_NameIndex": 5, + "m_Index": 1, + "m_SamplerIndex": 1, + "m_MultiSampled": false, + "m_Dim": 2 + } + ], + "m_BufferParams": [], + "m_ConstantBuffers": [ + { + "m_NameIndex": 6, + "m_MatrixParams": [], + "m_VectorParams": [ + { + "m_NameIndex": 7, + "m_Index": 112, + "m_ArraySize": 0, + "m_Type": 0, + "m_Dim": 4 + } + ], + "m_StructParams": [], + "m_Size": 144, + "m_IsPartialCB": true + } + ], + "m_ConstantBufferBindings": [ + { + "m_NameIndex": 6, + "m_Index": 0, + "m_ArraySize": 1 + } + ], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [ + 0, + 1, + 2, + 3 + ] + }, + "progGeometry": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progHull": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progDomain": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progRayTracing": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "m_HasInstancingVariant": false, + "m_HasProceduralInstancingVariant": false, + "m_UseName": "", + "m_Name": "", + "m_TextureName": "", + "m_Tags": { + "tags": [] + } + }, + { + "m_NameIndices": [ + { + "Key": "$Globals", + "Value": 0 + }, + { + "Key": "UnityPerDraw", + "Value": 1 + }, + { + "Key": "UnityPerFrame", + "Value": 2 + }, + { + "Key": "_CameraDepthTexture", + "Value": 6 + }, + { + "Key": "_CameraDepthTexture_TexelSize", + "Value": 3 + }, + { + "Key": "unity_MatrixVP", + "Value": 5 + }, + { + "Key": "unity_ObjectToWorld", + "Value": 4 + } + ], + "m_Type": 0, + "m_State": { + "m_Name": "", + "rtBlend0": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend1": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend2": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend3": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend4": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend5": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend6": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend7": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtSeparateBlend": false, + "zClip": { + "val": 1.0, + "name": "" + }, + "zTest": { + "val": 8.0, + "name": "" + }, + "zWrite": { + "val": 0.0, + "name": "" + }, + "culling": { + "val": 0.0, + "name": "" + }, + "conservative": { + "val": 0.0, + "name": "" + }, + "offsetFactor": { + "val": 0.0, + "name": "" + }, + "offsetUnits": { + "val": 0.0, + "name": "" + }, + "alphaToMask": { + "val": 0.0, + "name": "" + }, + "stencilOp": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilOpFront": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilOpBack": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilReadMask": { + "val": 255.0, + "name": "" + }, + "stencilWriteMask": { + "val": 255.0, + "name": "" + }, + "stencilRef": { + "val": 0.0, + "name": "" + }, + "fogStart": { + "val": 0.0, + "name": "unity_FogStart" + }, + "fogEnd": { + "val": 0.0, + "name": "unity_FogEnd" + }, + "fogDensity": { + "val": 0.0, + "name": "unity_FogDensity" + }, + "fogColor": { + "x": { + "val": 0.0, + "name": "" + }, + "y": { + "val": 0.0, + "name": "" + }, + "z": { + "val": 0.0, + "name": "" + }, + "w": { + "val": 0.0, + "name": "" + }, + "name": "unity_FogColor" + }, + "fogMode": -1, + "gpuProgramID": 311699, + "m_Tags": { + "tags": [] + }, + "m_LOD": 0, + "lighting": false + }, + "m_ProgramMask": 6, + "progVertex": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [ + [], + [], + [], + [ + { + "m_BlobIndex": 8, + "m_KeywordIndices": [], + "m_ShaderRequirements": 33, + "m_GpuProgramType": 15 + } + ] + ], + "m_ParameterBlobIndices": [ + [], + [], + [], + [ + 7 + ] + ], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [ + { + "m_NameIndex": 0, + "m_MatrixParams": [], + "m_VectorParams": [ + { + "m_NameIndex": 3, + "m_Index": 32, + "m_ArraySize": 0, + "m_Type": 0, + "m_Dim": 4 + } + ], + "m_StructParams": [], + "m_Size": 80, + "m_IsPartialCB": true + }, + { + "m_NameIndex": 1, + "m_MatrixParams": [ + { + "m_NameIndex": 4, + "m_Index": 0, + "m_ArraySize": 0, + "m_Type": 0, + "m_RowCount": 4 + } + ], + "m_VectorParams": [], + "m_StructParams": [], + "m_Size": 176, + "m_IsPartialCB": true + }, + { + "m_NameIndex": 2, + "m_MatrixParams": [ + { + "m_NameIndex": 5, + "m_Index": 272, + "m_ArraySize": 0, + "m_Type": 0, + "m_RowCount": 4 + } + ], + "m_VectorParams": [], + "m_StructParams": [], + "m_Size": 368, + "m_IsPartialCB": true + } + ], + "m_ConstantBufferBindings": [ + { + "m_NameIndex": 0, + "m_Index": 0, + "m_ArraySize": 1 + }, + { + "m_NameIndex": 1, + "m_Index": 1, + "m_ArraySize": 1 + }, + { + "m_NameIndex": 2, + "m_Index": 2, + "m_ArraySize": 1 + } + ], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [ + 0, + 1, + 2, + 3 + ] + }, + "progFragment": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [ + [], + [], + [], + [ + { + "m_BlobIndex": 10, + "m_KeywordIndices": [], + "m_ShaderRequirements": 33, + "m_GpuProgramType": 17 + } + ] + ], + "m_ParameterBlobIndices": [ + [], + [], + [], + [ + 9 + ] + ], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [ + { + "m_NameIndex": 6, + "m_Index": 0, + "m_SamplerIndex": 0, + "m_MultiSampled": false, + "m_Dim": 2 + } + ], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [ + 0, + 1, + 2, + 3 + ] + }, + "progGeometry": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progHull": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progDomain": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progRayTracing": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "m_HasInstancingVariant": false, + "m_HasProceduralInstancingVariant": false, + "m_UseName": "", + "m_Name": "", + "m_TextureName": "", + "m_Tags": { + "tags": [] + } + }, + { + "m_NameIndices": [ + { + "Key": "$Globals", + "Value": 0 + }, + { + "Key": "UnityPerCamera", + "Value": 9 + }, + { + "Key": "UnityPerDraw", + "Value": 1 + }, + { + "Key": "UnityPerFrame", + "Value": 2 + }, + { + "Key": "_CameraDepthTexture", + "Value": 6 + }, + { + "Key": "_HalfResColor", + "Value": 8 + }, + { + "Key": "_HalfResDepthBuffer", + "Value": 7 + }, + { + "Key": "_HalfResDepthBuffer_TexelSize", + "Value": 3 + }, + { + "Key": "_ZBufferParams", + "Value": 10 + }, + { + "Key": "unity_MatrixVP", + "Value": 5 + }, + { + "Key": "unity_ObjectToWorld", + "Value": 4 + } + ], + "m_Type": 0, + "m_State": { + "m_Name": "", + "rtBlend0": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend1": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend2": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend3": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend4": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend5": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend6": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend7": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtSeparateBlend": false, + "zClip": { + "val": 1.0, + "name": "" + }, + "zTest": { + "val": 8.0, + "name": "" + }, + "zWrite": { + "val": 0.0, + "name": "" + }, + "culling": { + "val": 0.0, + "name": "" + }, + "conservative": { + "val": 0.0, + "name": "" + }, + "offsetFactor": { + "val": 0.0, + "name": "" + }, + "offsetUnits": { + "val": 0.0, + "name": "" + }, + "alphaToMask": { + "val": 0.0, + "name": "" + }, + "stencilOp": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilOpFront": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilOpBack": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilReadMask": { + "val": 255.0, + "name": "" + }, + "stencilWriteMask": { + "val": 255.0, + "name": "" + }, + "stencilRef": { + "val": 0.0, + "name": "" + }, + "fogStart": { + "val": 0.0, + "name": "unity_FogStart" + }, + "fogEnd": { + "val": 0.0, + "name": "unity_FogEnd" + }, + "fogDensity": { + "val": 0.0, + "name": "unity_FogDensity" + }, + "fogColor": { + "x": { + "val": 0.0, + "name": "" + }, + "y": { + "val": 0.0, + "name": "" + }, + "z": { + "val": 0.0, + "name": "" + }, + "w": { + "val": 0.0, + "name": "" + }, + "name": "unity_FogColor" + }, + "fogMode": -1, + "gpuProgramID": 370087, + "m_Tags": { + "tags": [] + }, + "m_LOD": 0, + "lighting": false + }, + "m_ProgramMask": 6, + "progVertex": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [ + [], + [], + [], + [ + { + "m_BlobIndex": 11, + "m_KeywordIndices": [], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + } + ] + ], + "m_ParameterBlobIndices": [ + [], + [], + [], + [ + 7 + ] + ], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [ + { + "m_NameIndex": 0, + "m_MatrixParams": [], + "m_VectorParams": [ + { + "m_NameIndex": 3, + "m_Index": 48, + "m_ArraySize": 0, + "m_Type": 0, + "m_Dim": 4 + } + ], + "m_StructParams": [], + "m_Size": 80, + "m_IsPartialCB": true + }, + { + "m_NameIndex": 1, + "m_MatrixParams": [ + { + "m_NameIndex": 4, + "m_Index": 0, + "m_ArraySize": 0, + "m_Type": 0, + "m_RowCount": 4 + } + ], + "m_VectorParams": [], + "m_StructParams": [], + "m_Size": 176, + "m_IsPartialCB": true + }, + { + "m_NameIndex": 2, + "m_MatrixParams": [ + { + "m_NameIndex": 5, + "m_Index": 272, + "m_ArraySize": 0, + "m_Type": 0, + "m_RowCount": 4 + } + ], + "m_VectorParams": [], + "m_StructParams": [], + "m_Size": 368, + "m_IsPartialCB": true + } + ], + "m_ConstantBufferBindings": [ + { + "m_NameIndex": 0, + "m_Index": 0, + "m_ArraySize": 1 + }, + { + "m_NameIndex": 1, + "m_Index": 1, + "m_ArraySize": 1 + }, + { + "m_NameIndex": 2, + "m_Index": 2, + "m_ArraySize": 1 + } + ], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [ + 0, + 1, + 2, + 3 + ] + }, + "progFragment": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [ + [], + [], + [], + [ + { + "m_BlobIndex": 12, + "m_KeywordIndices": [], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + } + ] + ], + "m_ParameterBlobIndices": [ + [], + [], + [], + [ + 2 + ] + ], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [ + { + "m_NameIndex": 6, + "m_Index": 0, + "m_SamplerIndex": -1, + "m_MultiSampled": false, + "m_Dim": 2 + }, + { + "m_NameIndex": 7, + "m_Index": 1, + "m_SamplerIndex": 0, + "m_MultiSampled": false, + "m_Dim": 2 + }, + { + "m_NameIndex": 8, + "m_Index": 2, + "m_SamplerIndex": 1, + "m_MultiSampled": false, + "m_Dim": 2 + } + ], + "m_BufferParams": [], + "m_ConstantBuffers": [ + { + "m_NameIndex": 9, + "m_MatrixParams": [], + "m_VectorParams": [ + { + "m_NameIndex": 10, + "m_Index": 112, + "m_ArraySize": 0, + "m_Type": 0, + "m_Dim": 4 + } + ], + "m_StructParams": [], + "m_Size": 144, + "m_IsPartialCB": true + } + ], + "m_ConstantBufferBindings": [ + { + "m_NameIndex": 9, + "m_Index": 0, + "m_ArraySize": 1 + } + ], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [ + 0, + 1, + 2, + 3 + ] + }, + "progGeometry": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progHull": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progDomain": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progRayTracing": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "m_HasInstancingVariant": false, + "m_HasProceduralInstancingVariant": false, + "m_UseName": "", + "m_Name": "", + "m_TextureName": "", + "m_Tags": { + "tags": [] + } + }, + { + "m_NameIndices": [ + { + "Key": "$Globals", + "Value": 0 + }, + { + "Key": "UnityPerDraw", + "Value": 1 + }, + { + "Key": "UnityPerFrame", + "Value": 2 + }, + { + "Key": "_HalfResDepthBuffer", + "Value": 6 + }, + { + "Key": "_HalfResDepthBuffer_TexelSize", + "Value": 3 + }, + { + "Key": "unity_MatrixVP", + "Value": 5 + }, + { + "Key": "unity_ObjectToWorld", + "Value": 4 + } + ], + "m_Type": 0, + "m_State": { + "m_Name": "", + "rtBlend0": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend1": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend2": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend3": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend4": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend5": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend6": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend7": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtSeparateBlend": false, + "zClip": { + "val": 1.0, + "name": "" + }, + "zTest": { + "val": 8.0, + "name": "" + }, + "zWrite": { + "val": 0.0, + "name": "" + }, + "culling": { + "val": 0.0, + "name": "" + }, + "conservative": { + "val": 0.0, + "name": "" + }, + "offsetFactor": { + "val": 0.0, + "name": "" + }, + "offsetUnits": { + "val": 0.0, + "name": "" + }, + "alphaToMask": { + "val": 0.0, + "name": "" + }, + "stencilOp": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilOpFront": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilOpBack": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilReadMask": { + "val": 255.0, + "name": "" + }, + "stencilWriteMask": { + "val": 255.0, + "name": "" + }, + "stencilRef": { + "val": 0.0, + "name": "" + }, + "fogStart": { + "val": 0.0, + "name": "unity_FogStart" + }, + "fogEnd": { + "val": 0.0, + "name": "unity_FogEnd" + }, + "fogDensity": { + "val": 0.0, + "name": "unity_FogDensity" + }, + "fogColor": { + "x": { + "val": 0.0, + "name": "" + }, + "y": { + "val": 0.0, + "name": "" + }, + "z": { + "val": 0.0, + "name": "" + }, + "w": { + "val": 0.0, + "name": "" + }, + "name": "unity_FogColor" + }, + "fogMode": -1, + "gpuProgramID": 404775, + "m_Tags": { + "tags": [] + }, + "m_LOD": 0, + "lighting": false + }, + "m_ProgramMask": 6, + "progVertex": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [ + [], + [], + [], + [ + { + "m_BlobIndex": 13, + "m_KeywordIndices": [], + "m_ShaderRequirements": 33, + "m_GpuProgramType": 15 + } + ] + ], + "m_ParameterBlobIndices": [ + [], + [], + [], + [ + 7 + ] + ], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [ + { + "m_NameIndex": 0, + "m_MatrixParams": [], + "m_VectorParams": [ + { + "m_NameIndex": 3, + "m_Index": 48, + "m_ArraySize": 0, + "m_Type": 0, + "m_Dim": 4 + } + ], + "m_StructParams": [], + "m_Size": 80, + "m_IsPartialCB": true + }, + { + "m_NameIndex": 1, + "m_MatrixParams": [ + { + "m_NameIndex": 4, + "m_Index": 0, + "m_ArraySize": 0, + "m_Type": 0, + "m_RowCount": 4 + } + ], + "m_VectorParams": [], + "m_StructParams": [], + "m_Size": 176, + "m_IsPartialCB": true + }, + { + "m_NameIndex": 2, + "m_MatrixParams": [ + { + "m_NameIndex": 5, + "m_Index": 272, + "m_ArraySize": 0, + "m_Type": 0, + "m_RowCount": 4 + } + ], + "m_VectorParams": [], + "m_StructParams": [], + "m_Size": 368, + "m_IsPartialCB": true + } + ], + "m_ConstantBufferBindings": [ + { + "m_NameIndex": 0, + "m_Index": 0, + "m_ArraySize": 1 + }, + { + "m_NameIndex": 1, + "m_Index": 1, + "m_ArraySize": 1 + }, + { + "m_NameIndex": 2, + "m_Index": 2, + "m_ArraySize": 1 + } + ], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [ + 0, + 1, + 2, + 3 + ] + }, + "progFragment": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [ + [], + [], + [], + [ + { + "m_BlobIndex": 10, + "m_KeywordIndices": [], + "m_ShaderRequirements": 33, + "m_GpuProgramType": 17 + } + ] + ], + "m_ParameterBlobIndices": [ + [], + [], + [], + [ + 9 + ] + ], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [ + { + "m_NameIndex": 6, + "m_Index": 0, + "m_SamplerIndex": 0, + "m_MultiSampled": false, + "m_Dim": 2 + } + ], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [ + 0, + 1, + 2, + 3 + ] + }, + "progGeometry": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progHull": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progDomain": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progRayTracing": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "m_HasInstancingVariant": false, + "m_HasProceduralInstancingVariant": false, + "m_UseName": "", + "m_Name": "", + "m_TextureName": "", + "m_Tags": { + "tags": [] + } + }, + { + "m_NameIndices": [ + { + "Key": "$Globals", + "Value": 0 + }, + { + "Key": "UnityPerCamera", + "Value": 9 + }, + { + "Key": "UnityPerDraw", + "Value": 1 + }, + { + "Key": "UnityPerFrame", + "Value": 2 + }, + { + "Key": "_CameraDepthTexture", + "Value": 6 + }, + { + "Key": "_QuarterResColor", + "Value": 8 + }, + { + "Key": "_QuarterResDepthBuffer", + "Value": 7 + }, + { + "Key": "_QuarterResDepthBuffer_TexelSize", + "Value": 3 + }, + { + "Key": "_ZBufferParams", + "Value": 10 + }, + { + "Key": "unity_MatrixVP", + "Value": 5 + }, + { + "Key": "unity_ObjectToWorld", + "Value": 4 + } + ], + "m_Type": 0, + "m_State": { + "m_Name": "", + "rtBlend0": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend1": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend2": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend3": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend4": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend5": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend6": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend7": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtSeparateBlend": false, + "zClip": { + "val": 1.0, + "name": "" + }, + "zTest": { + "val": 8.0, + "name": "" + }, + "zWrite": { + "val": 0.0, + "name": "" + }, + "culling": { + "val": 0.0, + "name": "" + }, + "conservative": { + "val": 0.0, + "name": "" + }, + "offsetFactor": { + "val": 0.0, + "name": "" + }, + "offsetUnits": { + "val": 0.0, + "name": "" + }, + "alphaToMask": { + "val": 0.0, + "name": "" + }, + "stencilOp": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilOpFront": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilOpBack": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilReadMask": { + "val": 255.0, + "name": "" + }, + "stencilWriteMask": { + "val": 255.0, + "name": "" + }, + "stencilRef": { + "val": 0.0, + "name": "" + }, + "fogStart": { + "val": 0.0, + "name": "unity_FogStart" + }, + "fogEnd": { + "val": 0.0, + "name": "unity_FogEnd" + }, + "fogDensity": { + "val": 0.0, + "name": "unity_FogDensity" + }, + "fogColor": { + "x": { + "val": 0.0, + "name": "" + }, + "y": { + "val": 0.0, + "name": "" + }, + "z": { + "val": 0.0, + "name": "" + }, + "w": { + "val": 0.0, + "name": "" + }, + "name": "unity_FogColor" + }, + "fogMode": -1, + "gpuProgramID": 502837, + "m_Tags": { + "tags": [] + }, + "m_LOD": 0, + "lighting": false + }, + "m_ProgramMask": 6, + "progVertex": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [ + [], + [], + [], + [ + { + "m_BlobIndex": 14, + "m_KeywordIndices": [], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + } + ] + ], + "m_ParameterBlobIndices": [ + [], + [], + [], + [ + 7 + ] + ], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [ + { + "m_NameIndex": 0, + "m_MatrixParams": [], + "m_VectorParams": [ + { + "m_NameIndex": 3, + "m_Index": 64, + "m_ArraySize": 0, + "m_Type": 0, + "m_Dim": 4 + } + ], + "m_StructParams": [], + "m_Size": 80, + "m_IsPartialCB": true + }, + { + "m_NameIndex": 1, + "m_MatrixParams": [ + { + "m_NameIndex": 4, + "m_Index": 0, + "m_ArraySize": 0, + "m_Type": 0, + "m_RowCount": 4 + } + ], + "m_VectorParams": [], + "m_StructParams": [], + "m_Size": 176, + "m_IsPartialCB": true + }, + { + "m_NameIndex": 2, + "m_MatrixParams": [ + { + "m_NameIndex": 5, + "m_Index": 272, + "m_ArraySize": 0, + "m_Type": 0, + "m_RowCount": 4 + } + ], + "m_VectorParams": [], + "m_StructParams": [], + "m_Size": 368, + "m_IsPartialCB": true + } + ], + "m_ConstantBufferBindings": [ + { + "m_NameIndex": 0, + "m_Index": 0, + "m_ArraySize": 1 + }, + { + "m_NameIndex": 1, + "m_Index": 1, + "m_ArraySize": 1 + }, + { + "m_NameIndex": 2, + "m_Index": 2, + "m_ArraySize": 1 + } + ], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [ + 0, + 1, + 2, + 3 + ] + }, + "progFragment": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [ + [], + [], + [], + [ + { + "m_BlobIndex": 12, + "m_KeywordIndices": [], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + } + ] + ], + "m_ParameterBlobIndices": [ + [], + [], + [], + [ + 2 + ] + ], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [ + { + "m_NameIndex": 6, + "m_Index": 0, + "m_SamplerIndex": -1, + "m_MultiSampled": false, + "m_Dim": 2 + }, + { + "m_NameIndex": 7, + "m_Index": 1, + "m_SamplerIndex": 0, + "m_MultiSampled": false, + "m_Dim": 2 + }, + { + "m_NameIndex": 8, + "m_Index": 2, + "m_SamplerIndex": 1, + "m_MultiSampled": false, + "m_Dim": 2 + } + ], + "m_BufferParams": [], + "m_ConstantBuffers": [ + { + "m_NameIndex": 9, + "m_MatrixParams": [], + "m_VectorParams": [ + { + "m_NameIndex": 10, + "m_Index": 112, + "m_ArraySize": 0, + "m_Type": 0, + "m_Dim": 4 + } + ], + "m_StructParams": [], + "m_Size": 144, + "m_IsPartialCB": true + } + ], + "m_ConstantBufferBindings": [ + { + "m_NameIndex": 9, + "m_Index": 0, + "m_ArraySize": 1 + } + ], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [ + 0, + 1, + 2, + 3 + ] + }, + "progGeometry": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progHull": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progDomain": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progRayTracing": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "m_HasInstancingVariant": false, + "m_HasProceduralInstancingVariant": false, + "m_UseName": "", + "m_Name": "", + "m_TextureName": "", + "m_Tags": { + "tags": [] + } + }, + { + "m_NameIndices": [ + { + "Key": "UnityPerCamera", + "Value": 6 + }, + { + "Key": "UnityPerDraw", + "Value": 0 + }, + { + "Key": "UnityPerFrame", + "Value": 1 + }, + { + "Key": "_MainTex", + "Value": 5 + }, + { + "Key": "_QuarterResDepthBuffer", + "Value": 4 + }, + { + "Key": "_ZBufferParams", + "Value": 7 + }, + { + "Key": "unity_MatrixVP", + "Value": 3 + }, + { + "Key": "unity_ObjectToWorld", + "Value": 2 + } + ], + "m_Type": 0, + "m_State": { + "m_Name": "", + "rtBlend0": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend1": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend2": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend3": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend4": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend5": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend6": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend7": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtSeparateBlend": false, + "zClip": { + "val": 1.0, + "name": "" + }, + "zTest": { + "val": 8.0, + "name": "" + }, + "zWrite": { + "val": 0.0, + "name": "" + }, + "culling": { + "val": 0.0, + "name": "" + }, + "conservative": { + "val": 0.0, + "name": "" + }, + "offsetFactor": { + "val": 0.0, + "name": "" + }, + "offsetUnits": { + "val": 0.0, + "name": "" + }, + "alphaToMask": { + "val": 0.0, + "name": "" + }, + "stencilOp": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilOpFront": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilOpBack": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilReadMask": { + "val": 255.0, + "name": "" + }, + "stencilWriteMask": { + "val": 255.0, + "name": "" + }, + "stencilRef": { + "val": 0.0, + "name": "" + }, + "fogStart": { + "val": 0.0, + "name": "unity_FogStart" + }, + "fogEnd": { + "val": 0.0, + "name": "unity_FogEnd" + }, + "fogDensity": { + "val": 0.0, + "name": "unity_FogDensity" + }, + "fogColor": { + "x": { + "val": 0.0, + "name": "" + }, + "y": { + "val": 0.0, + "name": "" + }, + "z": { + "val": 0.0, + "name": "" + }, + "w": { + "val": 0.0, + "name": "" + }, + "name": "unity_FogColor" + }, + "fogMode": -1, + "gpuProgramID": 557656, + "m_Tags": { + "tags": [] + }, + "m_LOD": 0, + "lighting": false + }, + "m_ProgramMask": 6, + "progVertex": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [ + [], + [], + [], + [ + { + "m_BlobIndex": 1, + "m_KeywordIndices": [], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + } + ] + ], + "m_ParameterBlobIndices": [ + [], + [], + [], + [ + 0 + ] + ], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [ + { + "m_NameIndex": 0, + "m_MatrixParams": [ + { + "m_NameIndex": 2, + "m_Index": 0, + "m_ArraySize": 0, + "m_Type": 0, + "m_RowCount": 4 + } + ], + "m_VectorParams": [], + "m_StructParams": [], + "m_Size": 176, + "m_IsPartialCB": true + }, + { + "m_NameIndex": 1, + "m_MatrixParams": [ + { + "m_NameIndex": 3, + "m_Index": 272, + "m_ArraySize": 0, + "m_Type": 0, + "m_RowCount": 4 + } + ], + "m_VectorParams": [], + "m_StructParams": [], + "m_Size": 368, + "m_IsPartialCB": true + } + ], + "m_ConstantBufferBindings": [ + { + "m_NameIndex": 0, + "m_Index": 0, + "m_ArraySize": 1 + }, + { + "m_NameIndex": 1, + "m_Index": 1, + "m_ArraySize": 1 + } + ], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [ + 0, + 1, + 2, + 3 + ] + }, + "progFragment": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [ + [], + [], + [], + [ + { + "m_BlobIndex": 15, + "m_KeywordIndices": [], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + } + ] + ], + "m_ParameterBlobIndices": [ + [], + [], + [], + [ + 2 + ] + ], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [ + { + "m_NameIndex": 4, + "m_Index": 0, + "m_SamplerIndex": 0, + "m_MultiSampled": false, + "m_Dim": 2 + }, + { + "m_NameIndex": 5, + "m_Index": 1, + "m_SamplerIndex": 1, + "m_MultiSampled": false, + "m_Dim": 2 + } + ], + "m_BufferParams": [], + "m_ConstantBuffers": [ + { + "m_NameIndex": 6, + "m_MatrixParams": [], + "m_VectorParams": [ + { + "m_NameIndex": 7, + "m_Index": 112, + "m_ArraySize": 0, + "m_Type": 0, + "m_Dim": 4 + } + ], + "m_StructParams": [], + "m_Size": 144, + "m_IsPartialCB": true + } + ], + "m_ConstantBufferBindings": [ + { + "m_NameIndex": 6, + "m_Index": 0, + "m_ArraySize": 1 + } + ], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [ + 0, + 1, + 2, + 3 + ] + }, + "progGeometry": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progHull": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progDomain": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progRayTracing": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "m_HasInstancingVariant": false, + "m_HasProceduralInstancingVariant": false, + "m_UseName": "", + "m_Name": "", + "m_TextureName": "", + "m_Tags": { + "tags": [] + } + }, + { + "m_NameIndices": [ + { + "Key": "UnityPerCamera", + "Value": 6 + }, + { + "Key": "UnityPerDraw", + "Value": 0 + }, + { + "Key": "UnityPerFrame", + "Value": 1 + }, + { + "Key": "_MainTex", + "Value": 5 + }, + { + "Key": "_QuarterResDepthBuffer", + "Value": 4 + }, + { + "Key": "_ZBufferParams", + "Value": 7 + }, + { + "Key": "unity_MatrixVP", + "Value": 3 + }, + { + "Key": "unity_ObjectToWorld", + "Value": 2 + } + ], + "m_Type": 0, + "m_State": { + "m_Name": "", + "rtBlend0": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend1": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend2": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend3": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend4": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend5": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend6": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend7": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtSeparateBlend": false, + "zClip": { + "val": 1.0, + "name": "" + }, + "zTest": { + "val": 8.0, + "name": "" + }, + "zWrite": { + "val": 0.0, + "name": "" + }, + "culling": { + "val": 0.0, + "name": "" + }, + "conservative": { + "val": 0.0, + "name": "" + }, + "offsetFactor": { + "val": 0.0, + "name": "" + }, + "offsetUnits": { + "val": 0.0, + "name": "" + }, + "alphaToMask": { + "val": 0.0, + "name": "" + }, + "stencilOp": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilOpFront": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilOpBack": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilReadMask": { + "val": 255.0, + "name": "" + }, + "stencilWriteMask": { + "val": 255.0, + "name": "" + }, + "stencilRef": { + "val": 0.0, + "name": "" + }, + "fogStart": { + "val": 0.0, + "name": "unity_FogStart" + }, + "fogEnd": { + "val": 0.0, + "name": "unity_FogEnd" + }, + "fogDensity": { + "val": 0.0, + "name": "unity_FogDensity" + }, + "fogColor": { + "x": { + "val": 0.0, + "name": "" + }, + "y": { + "val": 0.0, + "name": "" + }, + "z": { + "val": 0.0, + "name": "" + }, + "w": { + "val": 0.0, + "name": "" + }, + "name": "unity_FogColor" + }, + "fogMode": -1, + "gpuProgramID": 627776, + "m_Tags": { + "tags": [] + }, + "m_LOD": 0, + "lighting": false + }, + "m_ProgramMask": 6, + "progVertex": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [ + [], + [], + [], + [ + { + "m_BlobIndex": 1, + "m_KeywordIndices": [], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + } + ] + ], + "m_ParameterBlobIndices": [ + [], + [], + [], + [ + 0 + ] + ], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [ + { + "m_NameIndex": 0, + "m_MatrixParams": [ + { + "m_NameIndex": 2, + "m_Index": 0, + "m_ArraySize": 0, + "m_Type": 0, + "m_RowCount": 4 + } + ], + "m_VectorParams": [], + "m_StructParams": [], + "m_Size": 176, + "m_IsPartialCB": true + }, + { + "m_NameIndex": 1, + "m_MatrixParams": [ + { + "m_NameIndex": 3, + "m_Index": 272, + "m_ArraySize": 0, + "m_Type": 0, + "m_RowCount": 4 + } + ], + "m_VectorParams": [], + "m_StructParams": [], + "m_Size": 368, + "m_IsPartialCB": true + } + ], + "m_ConstantBufferBindings": [ + { + "m_NameIndex": 0, + "m_Index": 0, + "m_ArraySize": 1 + }, + { + "m_NameIndex": 1, + "m_Index": 1, + "m_ArraySize": 1 + } + ], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [ + 0, + 1, + 2, + 3 + ] + }, + "progFragment": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [ + [], + [], + [], + [ + { + "m_BlobIndex": 16, + "m_KeywordIndices": [], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + } + ] + ], + "m_ParameterBlobIndices": [ + [], + [], + [], + [ + 2 + ] + ], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [ + { + "m_NameIndex": 4, + "m_Index": 0, + "m_SamplerIndex": 0, + "m_MultiSampled": false, + "m_Dim": 2 + }, + { + "m_NameIndex": 5, + "m_Index": 1, + "m_SamplerIndex": 1, + "m_MultiSampled": false, + "m_Dim": 2 + } + ], + "m_BufferParams": [], + "m_ConstantBuffers": [ + { + "m_NameIndex": 6, + "m_MatrixParams": [], + "m_VectorParams": [ + { + "m_NameIndex": 7, + "m_Index": 112, + "m_ArraySize": 0, + "m_Type": 0, + "m_Dim": 4 + } + ], + "m_StructParams": [], + "m_Size": 144, + "m_IsPartialCB": true + } + ], + "m_ConstantBufferBindings": [ + { + "m_NameIndex": 6, + "m_Index": 0, + "m_ArraySize": 1 + } + ], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [ + 0, + 1, + 2, + 3 + ] + }, + "progGeometry": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progHull": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progDomain": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progRayTracing": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "m_HasInstancingVariant": false, + "m_HasProceduralInstancingVariant": false, + "m_UseName": "", + "m_Name": "", + "m_TextureName": "", + "m_Tags": { + "tags": [] + } + }, + { + "m_NameIndices": [ + { + "Key": "$Globals", + "Value": 0 + }, + { + "Key": "UnityPerDraw", + "Value": 1 + }, + { + "Key": "UnityPerFrame", + "Value": 2 + }, + { + "Key": "_CameraDepthTexture", + "Value": 6 + }, + { + "Key": "_CameraDepthTexture_TexelSize", + "Value": 3 + }, + { + "Key": "unity_MatrixVP", + "Value": 5 + }, + { + "Key": "unity_ObjectToWorld", + "Value": 4 + } + ], + "m_Type": 0, + "m_State": { + "m_Name": "", + "rtBlend0": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend1": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend2": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend3": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend4": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend5": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend6": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend7": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtSeparateBlend": false, + "zClip": { + "val": 1.0, + "name": "" + }, + "zTest": { + "val": 8.0, + "name": "" + }, + "zWrite": { + "val": 0.0, + "name": "" + }, + "culling": { + "val": 0.0, + "name": "" + }, + "conservative": { + "val": 0.0, + "name": "" + }, + "offsetFactor": { + "val": 0.0, + "name": "" + }, + "offsetUnits": { + "val": 0.0, + "name": "" + }, + "alphaToMask": { + "val": 0.0, + "name": "" + }, + "stencilOp": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilOpFront": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilOpBack": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilReadMask": { + "val": 255.0, + "name": "" + }, + "stencilWriteMask": { + "val": 255.0, + "name": "" + }, + "stencilRef": { + "val": 0.0, + "name": "" + }, + "fogStart": { + "val": 0.0, + "name": "unity_FogStart" + }, + "fogEnd": { + "val": 0.0, + "name": "unity_FogEnd" + }, + "fogDensity": { + "val": 0.0, + "name": "unity_FogDensity" + }, + "fogColor": { + "x": { + "val": 0.0, + "name": "" + }, + "y": { + "val": 0.0, + "name": "" + }, + "z": { + "val": 0.0, + "name": "" + }, + "w": { + "val": 0.0, + "name": "" + }, + "name": "unity_FogColor" + }, + "fogMode": -1, + "gpuProgramID": 677594, + "m_Tags": { + "tags": [] + }, + "m_LOD": 0, + "lighting": false + }, + "m_ProgramMask": 6, + "progVertex": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [ + [], + [], + [], + [ + { + "m_BlobIndex": 8, + "m_KeywordIndices": [], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + } + ] + ], + "m_ParameterBlobIndices": [ + [], + [], + [], + [ + 7 + ] + ], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [ + { + "m_NameIndex": 0, + "m_MatrixParams": [], + "m_VectorParams": [ + { + "m_NameIndex": 3, + "m_Index": 32, + "m_ArraySize": 0, + "m_Type": 0, + "m_Dim": 4 + } + ], + "m_StructParams": [], + "m_Size": 80, + "m_IsPartialCB": true + }, + { + "m_NameIndex": 1, + "m_MatrixParams": [ + { + "m_NameIndex": 4, + "m_Index": 0, + "m_ArraySize": 0, + "m_Type": 0, + "m_RowCount": 4 + } + ], + "m_VectorParams": [], + "m_StructParams": [], + "m_Size": 176, + "m_IsPartialCB": true + }, + { + "m_NameIndex": 2, + "m_MatrixParams": [ + { + "m_NameIndex": 5, + "m_Index": 272, + "m_ArraySize": 0, + "m_Type": 0, + "m_RowCount": 4 + } + ], + "m_VectorParams": [], + "m_StructParams": [], + "m_Size": 368, + "m_IsPartialCB": true + } + ], + "m_ConstantBufferBindings": [ + { + "m_NameIndex": 0, + "m_Index": 0, + "m_ArraySize": 1 + }, + { + "m_NameIndex": 1, + "m_Index": 1, + "m_ArraySize": 1 + }, + { + "m_NameIndex": 2, + "m_Index": 2, + "m_ArraySize": 1 + } + ], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [ + 0, + 1, + 2, + 3 + ] + }, + "progFragment": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [ + [], + [], + [], + [ + { + "m_BlobIndex": 10, + "m_KeywordIndices": [], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + } + ] + ], + "m_ParameterBlobIndices": [ + [], + [], + [], + [ + 9 + ] + ], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [ + { + "m_NameIndex": 6, + "m_Index": 0, + "m_SamplerIndex": 0, + "m_MultiSampled": false, + "m_Dim": 2 + } + ], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [ + 0, + 1, + 2, + 3 + ] + }, + "progGeometry": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progHull": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progDomain": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progRayTracing": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "m_HasInstancingVariant": false, + "m_HasProceduralInstancingVariant": false, + "m_UseName": "", + "m_Name": "", + "m_TextureName": "", + "m_Tags": { + "tags": [] + } + }, + { + "m_NameIndices": [ + { + "Key": "$Globals", + "Value": 0 + }, + { + "Key": "UnityPerDraw", + "Value": 1 + }, + { + "Key": "UnityPerFrame", + "Value": 2 + }, + { + "Key": "_HalfResDepthBuffer", + "Value": 6 + }, + { + "Key": "_HalfResDepthBuffer_TexelSize", + "Value": 3 + }, + { + "Key": "unity_MatrixVP", + "Value": 5 + }, + { + "Key": "unity_ObjectToWorld", + "Value": 4 + } + ], + "m_Type": 0, + "m_State": { + "m_Name": "", + "rtBlend0": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend1": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend2": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend3": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend4": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend5": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend6": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend7": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtSeparateBlend": false, + "zClip": { + "val": 1.0, + "name": "" + }, + "zTest": { + "val": 8.0, + "name": "" + }, + "zWrite": { + "val": 0.0, + "name": "" + }, + "culling": { + "val": 0.0, + "name": "" + }, + "conservative": { + "val": 0.0, + "name": "" + }, + "offsetFactor": { + "val": 0.0, + "name": "" + }, + "offsetUnits": { + "val": 0.0, + "name": "" + }, + "alphaToMask": { + "val": 0.0, + "name": "" + }, + "stencilOp": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilOpFront": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilOpBack": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilReadMask": { + "val": 255.0, + "name": "" + }, + "stencilWriteMask": { + "val": 255.0, + "name": "" + }, + "stencilRef": { + "val": 0.0, + "name": "" + }, + "fogStart": { + "val": 0.0, + "name": "unity_FogStart" + }, + "fogEnd": { + "val": 0.0, + "name": "unity_FogEnd" + }, + "fogDensity": { + "val": 0.0, + "name": "unity_FogDensity" + }, + "fogColor": { + "x": { + "val": 0.0, + "name": "" + }, + "y": { + "val": 0.0, + "name": "" + }, + "z": { + "val": 0.0, + "name": "" + }, + "w": { + "val": 0.0, + "name": "" + }, + "name": "unity_FogColor" + }, + "fogMode": -1, + "gpuProgramID": 765538, + "m_Tags": { + "tags": [] + }, + "m_LOD": 0, + "lighting": false + }, + "m_ProgramMask": 6, + "progVertex": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [ + [], + [], + [], + [ + { + "m_BlobIndex": 13, + "m_KeywordIndices": [], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + } + ] + ], + "m_ParameterBlobIndices": [ + [], + [], + [], + [ + 7 + ] + ], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [ + { + "m_NameIndex": 0, + "m_MatrixParams": [], + "m_VectorParams": [ + { + "m_NameIndex": 3, + "m_Index": 48, + "m_ArraySize": 0, + "m_Type": 0, + "m_Dim": 4 + } + ], + "m_StructParams": [], + "m_Size": 80, + "m_IsPartialCB": true + }, + { + "m_NameIndex": 1, + "m_MatrixParams": [ + { + "m_NameIndex": 4, + "m_Index": 0, + "m_ArraySize": 0, + "m_Type": 0, + "m_RowCount": 4 + } + ], + "m_VectorParams": [], + "m_StructParams": [], + "m_Size": 176, + "m_IsPartialCB": true + }, + { + "m_NameIndex": 2, + "m_MatrixParams": [ + { + "m_NameIndex": 5, + "m_Index": 272, + "m_ArraySize": 0, + "m_Type": 0, + "m_RowCount": 4 + } + ], + "m_VectorParams": [], + "m_StructParams": [], + "m_Size": 368, + "m_IsPartialCB": true + } + ], + "m_ConstantBufferBindings": [ + { + "m_NameIndex": 0, + "m_Index": 0, + "m_ArraySize": 1 + }, + { + "m_NameIndex": 1, + "m_Index": 1, + "m_ArraySize": 1 + }, + { + "m_NameIndex": 2, + "m_Index": 2, + "m_ArraySize": 1 + } + ], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [ + 0, + 1, + 2, + 3 + ] + }, + "progFragment": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [ + [], + [], + [], + [ + { + "m_BlobIndex": 10, + "m_KeywordIndices": [], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + } + ] + ], + "m_ParameterBlobIndices": [ + [], + [], + [], + [ + 9 + ] + ], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [ + { + "m_NameIndex": 6, + "m_Index": 0, + "m_SamplerIndex": 0, + "m_MultiSampled": false, + "m_Dim": 2 + } + ], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [ + 0, + 1, + 2, + 3 + ] + }, + "progGeometry": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progHull": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progDomain": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progRayTracing": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "m_HasInstancingVariant": false, + "m_HasProceduralInstancingVariant": false, + "m_UseName": "", + "m_Name": "", + "m_TextureName": "", + "m_Tags": { + "tags": [] + } + } + ], + "m_Tags": { + "tags": [] + }, + "m_LOD": 0 + } + ], + "m_KeywordNames": [ + "STEREO_INSTANCING_ON", + "UNITY_SINGLE_PASS_STEREO", + "STEREO_MULTIVIEW_ON", + "STEREO_CUBEMAP_RENDER_ON" + ], + "m_KeywordFlags": "AAAAAA==", + "m_Name": "Hidden/BilateralBlur", + "m_CustomEditorName": "", + "m_FallbackName": "", + "m_Dependencies": [], + "m_CustomEditorForRenderPipelines": [], + "m_DisableNoSubshadersMessage": false + }, + "platforms": [ + 4 + ], + "offsets": [ + [ + 0 + ] + ], + "compressedLengths": [ + [ + 3760 + ] + ], + "decompressedLengths": [ + [ + 41308 + ] + ], + "compressedBlob": "ohEAAADQAAAAWAABAGIoAQAA5AIMAFMMBAAAPBgAYkgEAACQGAwAJtgcDABiaDUAAEASDAAmqEcMAFPoWQAAcDwAYlhaAAAcBAwAU3ReAAAcGABTkF4AAEAYAFPQYgAAgAwAYlBnAACgBgwAJvBtPAAmDHIkAGKMdgAAaBUMACb0iwwAbrp1CgwDAAEA9wIMAAAAVW5pdHlQZXJEcmF3sBwAFw0cAKpGcmFtZQAAAHABQAAAWABXDwAAAAgUAAMnATAAqgIMAC8CAAEAD/AFRFhCQ86YsEF7o5VLQKikADFwi7hpAECEAgAAtgBQLAAAAIBiAYMAAABJU0dOTGYAAHYAFzhIAATiAFsPBwAAQRgAAFAA/w4DAwAAUE9TSVRJT04AVEVYQ09PUkQAq6tPU0dOUFQADCYDDFQAFwFUAAD+AAVLADVTVl9gAPELq6urU0hEUqQBAABAAAEAaQAAAFkAAARGjiBEABcEEAAASAAAqQGTXwAAA3IQEAAADAAxMhAQHAAQZQwAEiAYAGJnAAAE8iAYAACAAEBoAAACuABENgAABSQAE0Y4AGI4AAAI8gAUACNWFRwAJY4gxABqMgAACvIAGAAAAQATBoQAIkYOCAAPKAABAHQAKqYaKAAXAHAAKkYOMAAXA5AAAHwAJlYFIAAAEAAXEmgAFwEYAACKAxcGVAAfASgABBATkAAfCigAABogKABqFAAAAPYPKAARPg8BFxG8AgSiARMF5AIPxgIBFw4gA65DYW1lcmEAAJAAIAMiEQAYABMeFAATBQgAIGYYuwEvAQIgAxPwAd0VtE0Q9bwdzNczw7KVrVy4ACZAGCADU4QAAAC4IAMPzAIOHwPMAh8AJAMTLCgAAFgAHyB4AwEgAAA3AHJUYXJnZXQAAAMggBcAA0oAAOAFAAMARAAQWjsAEmCMAQQMAABgAGJYGAAEAHAYACZVVRAAEwEQACJiEBwDEwAcAxPy+AIADAMAXgGmRQAAigEWAADyABADAQgAEn5EAARoAAEoABIUKAAfASgACxAJJAAfAkwADRISTAAfAygAFBsEKAATACgAA3QDIwuCwAAiKoA0AVMHAAAACjgAFzoUAEQOAAAKLAAQAm0BOACAPwQAEzocAATIAA94ABEfAXgAGB8BeAAIAzEGFAigAEQ6ABCAfAIEIAAbOCAAFIGcAgFUAGcAPzgAAAcgAAkIAA8cAAEAOACfO6q4vxkAAAWCMAAEGwMwAI/gW+M8MgAACUwAAQAcAAAIAEAkFK89QAAUcuQBEgIIAABQBBIDPAEkDHIoAhICCAATAjgABAQAFwA4AAC0BSaCIHAAGwKUAh8DqAEQHwKoAQAfA6gBBB8CqAEIFwKoARcCnAEEsAEXAqgBFwKoAR8CqAEABEAACAgADxwAAQyoAQwwABcSqAEEMABMbDcZPagBCBwAJToAIAEjCXLEABcGsAEXAZwBCCgEHwJsAQwfEmwBBB8CbAEAHxJsAQgXCmwBGxJsARsKbAEEIAAfCmwBBAcgAAkIAA8cAAEIbAEfEjAAAB8iMAAAOYU8RWwBDBwADGwBEwDYBwQAAxcAdAEBbAE/GAAASAUSHxgAAxMeEqgEDwADBgQsAA+UAQUXAZQBCIwBGwGoBAggABOBIAAMlAEDIAAJCAAPHAABDJQBDDAAHyIwAAA9HIJylAEIHAAPlAEFCPgCJUYCEAY/igEalAEUHxqUAf8IPwtljpQBBgAcAA+UAR4fHJQBFB8clAH/CD9bt5+UAQYAHAAPlAEeHx6UARQfHpQB/wg/1BqrlAEGABwAD5QBHh8ClAEUHwKUAf9ZHwSUARQfBLwE/1kfBpQBFB8G5Af/WR8IlAEUHwgMC/9ZHwqUARQfCpQB/wgPNA4BHgE0Dg+UARcfDJQBFB8MlAH/CAw0EQiUAQ40EQ+UARAfDpQBFB8OlAH/CAxwFAwcAA+UARkQDpQUBPwZAxQABOQYAtwYD5AYP/8Apq7FqVfGqESXyL9IkxiYkBj/EC9gAZAYEy9AAZAYNy8gAZAYEy8gAZAY//8NL0ABkBj/WC9gAZAY/1gvgAHICBMvgAGEDf9YH6CUARQvoAGsEP9YH8CUARQvwAHUE/9YH+CUARQv4AH8Fv9YHyAYDxQfIJAY/1kfQJQBFB9AkBj/WR9glAEUH2CQGP9ZH4CUARQvgAAMC/9ZD5QBFB+gkBj/WR/AlAEUH8CQGP9ZH+CUARQf4JAY/4ZXaAAAABZ4MQABAC8WEpAYGfABpCNCQwe4p/wF5/trIvA7MGUAL/ARkBiPIjARkBgvTASQGFMbBGQrD5AYDi8WAJAYCw5ILQ9EGCIfAkQYLB8CRBgkHwJEGLQIvBUEMABPtyYfPUQYBQAcAAAIADUzHPVEGBcBIBUWBgAYD2waDgRoGBsBaBgAXAAEBAAIfBUPaBgGHxiML/8IP6adbvwWBgAcAA/8FhYP+C3/OT+Ye6PICAYAHAAP+C3/Yj+0u8yUAQYAHAAP+C3/Yj8fU+qUAQYAHAAP+C3/Yg+UAQwP+C3/Yw+8BAwP+C3/Yw/kBwwP+C3/Yw8MCwEeAQwLD/gt/1sMbA4MHAAPQBKN/wExQDjqAHtA1JVhYk693QhAQBL/Dx+A0CoUL2ABQBITL2ABQBL//zEvgAFAEv9YD6gnFw9cCv8mD6gn/2MPhA0MD6gn/2MPrBAMD6gn/2MPlAEMD6gn/2MPvAQMD6gn/2MP5AcMD6gn/2MPQBIMD6gnSA9AEv+CLgQAAQAAPhGfJEdsb2JhbHNQMFlAHwwwWQAg4gMMAC8DALASE/ABXDlV+Z4u/DWgn8pd2gGi7mkAKrwDMFkvIAEwWUcTmDo+EwhsAA8wWQEAGAAbARgAAIdaF4D2AAVsAAMwABsDGAAAMAAbiUwTHwJ4WRAilAJ4WRuleFkbA3hZCIhZHwKIWRgADAAXwgwAABgAFwEYABcBrFkeAqxZIwZSxAImBoFQWVQyAAAOMggRR4AggEEiAhMCjAMDBAAFeBMDYAEVB6ASA1gAJhoALBEEwBMmRgwUABcizBEDAQAUB8QAFEbYEwdABATUACMGBAgAFoS4AA9oWgUBwAMPQFoAHwFoWhQfAWhaHB8BaFoMHwJoWgQfAmhaFB8CaFoMFwIoAA9oWiMfAfIDBAToFhcN4BYEUgOPFgQAAAIBAAE4BBPxAavP5Pv39Pqa5b2BNMSAranoFgc4BBPMaQMfSeQDER8D5AMEHwzkAwQfA+QDBB8M5AMEHwMwFysuAQ4wFyLoAjAXG7ogFwwUFwgEFwAMABfCDAAEEAQEGAAADABmZCAABDIQ7AMACAQXEjgXA5QAFQe4AhMUMAIDCAAfHdQCACbmDhgWTDcAAA8gAAMUBBNABAABHAQDFAATwAQAEL8EAAB0BxPCPAATpjwAEwZ4AEwaAAAFHAAAMAATMhQAFObsAgMUBBAbaF4aABQAEx4ECAR4BATABxIAmAErBxIUAAB0BAjIQh8BEAkICEAWL+YaJAADEzNUCBsAoAgANABfNAAAB0IcAAQMgAAfAYAAEB8BgAAEH4KAAAgOPAkOnAAbKkAACNQAGzpcAUQ3AAAJXAIbChwALioA6AgPeAgLL0YEeAgZ8AHfESb8/yfTFX3qQhIEuMM1ZQAqIAR4CB84eAhIEbCoBQJUAB+YeAgEH5h4CAQfmHgIBB+YeAgEG5huBRMCMAAboawEHwOQCBAT4JAIHrgIYg+QCFYEGAAXApwIHwNIYhAEmAgbAJgIHwOYCAwATAAElAAAOAgTAPwIBZADFoCUCQHUGQ+UCAAEvAAtpg5IAAVcDAOoDAFcAARcDBcaXAAP3AgNBBQBBRwABzwAD9wI9R8D3AguABwAExC6DhcCWgMgdgYfBw+oHxXwAS6v7AAz0NFAFtHP9sfNKgp8ACZQBoAEU+QAAAAYgAQPLAQOHwMsBAQfDCwEBB8DLAQEHwwsBAQfAywEAA8IIEUTBQggHwEIIDcEEAAfAhQJJAQYAB8CSCAHHglEIA8QEQAP9B8EHwAQEQEP9B8IBDQACPgHLuYasBEEeAAfIngABB8BiBEcLxoATAgED3gAHA+gHgEfIngAHC9GEHgAMx9CeAAgHwJ4ADAfgngAEAhEBRcGXCE0Jg0QVGgFZBMmlgxcIQ80AgEfMeASBKcAAMA/HwAEAwoAlE0eIMwCFwIEFHUVAAABHwAANAAQMcgJBrgPBeQhFxoMAADUBgWwIAoYACMFYhgAJaYbcAcUBqRKHxosAAATAywABEQKCJBKDEgTEgOgACsIghgUCKQKBHAAA9wLDXAAAYABEhEsABc3uBMAPAYfAVggABMxiBQM9CIXCvgKBIgIBdAGA1gCE5YMBwmIAR4AiAETAIgBDiALD5gTM/8B/ppZUbjdRrYTm5C1LOd4LJgT/xYfBJgThB4DmBMPAAsWD5gTaR8DmBP/Ow88DzP/AZUlOKIPhZw76frTdGlRTMs8D/8uHwU8D6gfBDwPNB8EPA9IHwQ8DygfBDwP/z8SfK4VCJoBPwA+FeQuGfABZ6FzhDtW+FlCWchuF8bASHwALxgV5C6PIlgU3A4vFgXkLlwPRHI7BSgADyRB/3o/Q6AE5C4GABwAAAgAPypCzOQuRgBcAAQEAA/kLgcPsHD/FD9dCDvkLgYAHAAPsHD/Yj8tx3fYIwYAHAAPsHD/Yj+wLpqUAQYAHAAPuEL/Yj/oQbSUAQYAHAAPuEL/Yj9d+cWUAQYAHAAPuEL/Yg+UAQwPuEL/Yw+8BAwPuEL/Yw/kBwwPuEL/Yw8MCwwPuEL/Yw80DgEeATQOD7Bw/1sMlBEMHAAPTERLD2gVL/8BTSNu7D2MLarcUa67vmVvLWgV/w8PHG87BSgAD2gV//8sL2ABaBX/Jg+Ibf9jD1wKDA+Ibf9jD4QNDA/gRf9jD6wQDA/gRf9jD9QTDA/gRf9jD5QBDA/gRf9jD7wEDA/gRf9jD+QHDA/gRf9jDwwLDA/gRf9jD2gVDA+IbUgPaBX/eVAAAAAAAA==", + "stageCounts": [ + 2 + ], + "m_Dependencies": [], + "m_NonModifiableTextures": [], + "m_ShaderIsBaked": true, + "m_AssetGUID": { + "data[0]": 812670962, + "data[1]": 1129067404, + "data[2]": 4084889483, + "data[3]": 543107941 + } +} \ No newline at end of file diff --git a/tests/Fixtures/VolumetricLights/Json/BlitAdd.json b/tests/Fixtures/VolumetricLights/Json/BlitAdd.json new file mode 100644 index 0000000..c10325a --- /dev/null +++ b/tests/Fixtures/VolumetricLights/Json/BlitAdd.json @@ -0,0 +1,755 @@ +{ + "m_Name": "", + "m_ParsedForm": { + "m_PropInfo": { + "m_Props": [ + { + "m_Name": "_MainTex", + "m_Description": "Texture", + "m_Attributes": [], + "m_Type": 4, + "m_Flags": 0, + "m_DefValue[0]": 0.0, + "m_DefValue[1]": 0.0, + "m_DefValue[2]": 0.0, + "m_DefValue[3]": 0.0, + "m_DefTexture": { + "m_DefaultName": "", + "m_TexDim": 1 + } + } + ] + }, + "m_SubShaders": [ + { + "m_Passes": [ + { + "m_NameIndices": [ + { + "Key": "$Globals", + "Value": 0 + }, + { + "Key": "UnityPerDraw", + "Value": 1 + }, + { + "Key": "UnityPerFrame", + "Value": 2 + }, + { + "Key": "_MainTex", + "Value": 6 + }, + { + "Key": "_MainTex_ST", + "Value": 3 + }, + { + "Key": "_Source", + "Value": 7 + }, + { + "Key": "_Source_TexelSize", + "Value": 8 + }, + { + "Key": "unity_MatrixVP", + "Value": 5 + }, + { + "Key": "unity_ObjectToWorld", + "Value": 4 + } + ], + "m_Type": 0, + "m_State": { + "m_Name": "", + "rtBlend0": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend1": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend2": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend3": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend4": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend5": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend6": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend7": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtSeparateBlend": false, + "zClip": { + "val": 1.0, + "name": "" + }, + "zTest": { + "val": 8.0, + "name": "" + }, + "zWrite": { + "val": 0.0, + "name": "" + }, + "culling": { + "val": 0.0, + "name": "" + }, + "conservative": { + "val": 0.0, + "name": "" + }, + "offsetFactor": { + "val": 0.0, + "name": "" + }, + "offsetUnits": { + "val": 0.0, + "name": "" + }, + "alphaToMask": { + "val": 0.0, + "name": "" + }, + "stencilOp": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilOpFront": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilOpBack": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilReadMask": { + "val": 255.0, + "name": "" + }, + "stencilWriteMask": { + "val": 255.0, + "name": "" + }, + "stencilRef": { + "val": 0.0, + "name": "" + }, + "fogStart": { + "val": 0.0, + "name": "unity_FogStart" + }, + "fogEnd": { + "val": 0.0, + "name": "unity_FogEnd" + }, + "fogDensity": { + "val": 0.0, + "name": "unity_FogDensity" + }, + "fogColor": { + "x": { + "val": 0.0, + "name": "" + }, + "y": { + "val": 0.0, + "name": "" + }, + "z": { + "val": 0.0, + "name": "" + }, + "w": { + "val": 0.0, + "name": "" + }, + "name": "unity_FogColor" + }, + "fogMode": -1, + "gpuProgramID": 17230, + "m_Tags": { + "tags": [] + }, + "m_LOD": 0, + "lighting": false + }, + "m_ProgramMask": 6, + "progVertex": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [ + [], + [], + [], + [ + { + "m_BlobIndex": 1, + "m_KeywordIndices": [], + "m_ShaderRequirements": 33, + "m_GpuProgramType": 15 + } + ] + ], + "m_ParameterBlobIndices": [ + [], + [], + [], + [ + 0 + ] + ], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [ + { + "m_NameIndex": 0, + "m_MatrixParams": [], + "m_VectorParams": [ + { + "m_NameIndex": 3, + "m_Index": 32, + "m_ArraySize": 0, + "m_Type": 0, + "m_Dim": 4 + } + ], + "m_StructParams": [], + "m_Size": 64, + "m_IsPartialCB": true + }, + { + "m_NameIndex": 1, + "m_MatrixParams": [ + { + "m_NameIndex": 4, + "m_Index": 0, + "m_ArraySize": 0, + "m_Type": 0, + "m_RowCount": 4 + } + ], + "m_VectorParams": [], + "m_StructParams": [], + "m_Size": 176, + "m_IsPartialCB": true + }, + { + "m_NameIndex": 2, + "m_MatrixParams": [ + { + "m_NameIndex": 5, + "m_Index": 272, + "m_ArraySize": 0, + "m_Type": 0, + "m_RowCount": 4 + } + ], + "m_VectorParams": [], + "m_StructParams": [], + "m_Size": 368, + "m_IsPartialCB": true + } + ], + "m_ConstantBufferBindings": [ + { + "m_NameIndex": 0, + "m_Index": 0, + "m_ArraySize": 1 + }, + { + "m_NameIndex": 1, + "m_Index": 1, + "m_ArraySize": 1 + }, + { + "m_NameIndex": 2, + "m_Index": 2, + "m_ArraySize": 1 + } + ], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [ + 0, + 1, + 2, + 3 + ] + }, + "progFragment": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [ + [], + [], + [], + [ + { + "m_BlobIndex": 3, + "m_KeywordIndices": [], + "m_ShaderRequirements": 33, + "m_GpuProgramType": 17 + } + ] + ], + "m_ParameterBlobIndices": [ + [], + [], + [], + [ + 2 + ] + ], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [ + { + "m_NameIndex": 6, + "m_Index": 0, + "m_SamplerIndex": 0, + "m_MultiSampled": false, + "m_Dim": 2 + }, + { + "m_NameIndex": 7, + "m_Index": 1, + "m_SamplerIndex": 1, + "m_MultiSampled": false, + "m_Dim": 2 + } + ], + "m_BufferParams": [], + "m_ConstantBuffers": [ + { + "m_NameIndex": 0, + "m_MatrixParams": [], + "m_VectorParams": [ + { + "m_NameIndex": 8, + "m_Index": 48, + "m_ArraySize": 0, + "m_Type": 0, + "m_Dim": 4 + } + ], + "m_StructParams": [], + "m_Size": 64, + "m_IsPartialCB": true + } + ], + "m_ConstantBufferBindings": [ + { + "m_NameIndex": 0, + "m_Index": 0, + "m_ArraySize": 1 + } + ], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [ + 0, + 1, + 2, + 3 + ] + }, + "progGeometry": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progHull": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progDomain": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progRayTracing": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "m_HasInstancingVariant": false, + "m_HasProceduralInstancingVariant": false, + "m_UseName": "", + "m_Name": "", + "m_TextureName": "", + "m_Tags": { + "tags": [] + } + } + ], + "m_Tags": { + "tags": [] + }, + "m_LOD": 0 + } + ], + "m_KeywordNames": [ + "STEREO_INSTANCING_ON", + "UNITY_SINGLE_PASS_STEREO", + "STEREO_MULTIVIEW_ON", + "STEREO_CUBEMAP_RENDER_ON" + ], + "m_KeywordFlags": "AAAAAA==", + "m_Name": "Hidden/BlitAdd", + "m_CustomEditorName": "", + "m_FallbackName": "", + "m_Dependencies": [], + "m_CustomEditorForRenderPipelines": [], + "m_DisableNoSubshadersMessage": true + }, + "platforms": [ + 4 + ], + "offsets": [ + [ + 0 + ] + ], + "compressedLengths": [ + [ + 792 + ] + ], + "decompressedLengths": [ + [ + 1660 + ] + ], + "compressedBlob": "ogQAAAA0AAAAcAABAGKkAAAADAMMACKwAxwAABgAYuQDAACYAgwAbrp1CgwEAAEA1wgAAAAkR2xvYmFsc0AYAPcCDAAAAFVuaXR5UGVyRHJhd7AcABcNHABQRnJhbWWMABoBWAAAcABXDwAAAAkUAAOLADAA0gIMAC8DAAEAD/AFRFhCQzbRTVK3WPjUuKXMhu7vQmZpADGsAgBAAPMCLAAAAIAAAADYAAAASVNHTkxmAADWABc4SAAEbABbDwcAAEEYAABQAP8OAwMAAFBPU0lUSU9OAFRFWENPT1JEAKurT1NHTlBUAAAYAVQAAKgAD1QAAW8MAABTVl9XAAH1CatTSERSzAEAAEAAAQBzAAAAWQAABEaOIEAABBAAAEgAFwQQAACIANMVAAAAXwAAA3IQEAAADAAxMhAQLAB1ZwAABPIgEKQAEGUcABIgHABSaAAAAgLEADII8gAkACJWFQgABGwAAAQAajIAAAryABgAAAEAEwZwACJGDggADygAAQBgACqmGigAFwBwACpGDjAAFwOQAAAQACZWBSAAAEwAFxJoABcBGAAQEZAAB1QAHwEoAAQQE5AAHwooAAAaIGgAbRQAAAD2DygAFAs8ARNGYAE1RoAgygIm5ooMABE+OwEXEeQCEwC6ARMFDAMfAnwDGARAAxMRRAAbAkADEG5AAz8CAQJAAxPwAQV6ZN2iz2Dvkef72Xj03YHIABdIQANThAAAALhAAw/sAiYfA+wCBwBEAxMsKAAAWAAfIJgDAREAQAByVGFyZ2V0ACADEYggAzsAAGIgAwASARBaOwASYLABBAwAAGAAYlgYAAQAcBgAJlVVEAATARAAJmIQLAMAHAMECAIEHANTMQAACBJYAjYagCDkAAafASQIIiAAQxAQgEHYAAIgAGSAPzcAAAkgABQKKAAGMAABLABENgAABWQAEwoUAEVFAAAJJAMECAASfhwAKgBgJAAJkAITfmQDAwwBUzgAAAeCQAAXOggAA9ACJAly+AISAggAAPQCEwEQAAScACqCIEAAAtwCgAAAAAAAAAAA", + "stageCounts": [ + 2 + ], + "m_Dependencies": [], + "m_NonModifiableTextures": [], + "m_ShaderIsBaked": true, + "m_AssetGUID": { + "data[0]": 3005331526, + "data[1]": 1141387252, + "data[2]": 153476519, + "data[3]": 3326652007 + } +} \ No newline at end of file diff --git a/tests/Fixtures/VolumetricLights/Json/VolumetricLight.json b/tests/Fixtures/VolumetricLights/Json/VolumetricLight.json new file mode 100644 index 0000000..26bfa3a --- /dev/null +++ b/tests/Fixtures/VolumetricLights/Json/VolumetricLight.json @@ -0,0 +1,5972 @@ +{ + "m_Name": "", + "m_ParsedForm": { + "m_PropInfo": { + "m_Props": [ + { + "m_Name": "_MainTex", + "m_Description": "Texture", + "m_Attributes": [], + "m_Type": 4, + "m_Flags": 1, + "m_DefValue[0]": 0.0, + "m_DefValue[1]": 0.0, + "m_DefValue[2]": 0.0, + "m_DefValue[3]": 0.0, + "m_DefTexture": { + "m_DefaultName": "white", + "m_TexDim": 2 + } + }, + { + "m_Name": "_ZTest", + "m_Description": "ZTest", + "m_Attributes": [], + "m_Type": 2, + "m_Flags": 1, + "m_DefValue[0]": 0.0, + "m_DefValue[1]": 0.0, + "m_DefValue[2]": 0.0, + "m_DefValue[3]": 0.0, + "m_DefTexture": { + "m_DefaultName": "", + "m_TexDim": 1 + } + }, + { + "m_Name": "_LightColor", + "m_Description": "_LightColor", + "m_Attributes": [], + "m_Type": 0, + "m_Flags": 1, + "m_DefValue[0]": 1.0, + "m_DefValue[1]": 1.0, + "m_DefValue[2]": 1.0, + "m_DefValue[3]": 1.0, + "m_DefTexture": { + "m_DefaultName": "", + "m_TexDim": 1 + } + } + ] + }, + "m_SubShaders": [ + { + "m_Passes": [ + { + "m_NameIndices": [ + { + "Key": "$Globals", + "Value": 0 + }, + { + "Key": "UnityPerCamera", + "Value": 1 + }, + { + "Key": "UnityPerDraw", + "Value": 2 + }, + { + "Key": "_ProjectionParams", + "Value": 4 + }, + { + "Key": "_WorldViewProj", + "Value": 3 + }, + { + "Key": "unity_ObjectToWorld", + "Value": 5 + } + ], + "m_Type": 0, + "m_State": { + "m_Name": "", + "rtBlend0": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 1.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 1.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend1": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend2": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend3": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend4": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend5": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend6": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend7": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtSeparateBlend": false, + "zClip": { + "val": 1.0, + "name": "" + }, + "zTest": { + "val": 0.0, + "name": "" + }, + "zWrite": { + "val": 0.0, + "name": "" + }, + "culling": { + "val": 1.0, + "name": "" + }, + "conservative": { + "val": 0.0, + "name": "" + }, + "offsetFactor": { + "val": 0.0, + "name": "" + }, + "offsetUnits": { + "val": 0.0, + "name": "" + }, + "alphaToMask": { + "val": 0.0, + "name": "" + }, + "stencilOp": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilOpFront": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilOpBack": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilReadMask": { + "val": 255.0, + "name": "" + }, + "stencilWriteMask": { + "val": 255.0, + "name": "" + }, + "stencilRef": { + "val": 0.0, + "name": "" + }, + "fogStart": { + "val": 0.0, + "name": "unity_FogStart" + }, + "fogEnd": { + "val": 0.0, + "name": "unity_FogEnd" + }, + "fogDensity": { + "val": 0.0, + "name": "unity_FogDensity" + }, + "fogColor": { + "x": { + "val": 0.0, + "name": "" + }, + "y": { + "val": 0.0, + "name": "" + }, + "z": { + "val": 0.0, + "name": "" + }, + "w": { + "val": 0.0, + "name": "" + }, + "name": "unity_FogColor" + }, + "fogMode": -1, + "gpuProgramID": 48854, + "m_Tags": { + "tags": [ + { + "Key": "RenderType", + "Value": "Opaque" + } + ] + }, + "m_LOD": 100, + "lighting": false + }, + "m_ProgramMask": 6, + "progVertex": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [ + [], + [], + [], + [ + { + "m_BlobIndex": 1, + "m_KeywordIndices": [], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 2, + "m_KeywordIndices": [ + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 3, + "m_KeywordIndices": [ + 7 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 4, + "m_KeywordIndices": [ + 7, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 5, + "m_KeywordIndices": [ + 6 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 6, + "m_KeywordIndices": [ + 6, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 7, + "m_KeywordIndices": [ + 6, + 7 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 8, + "m_KeywordIndices": [ + 6, + 7, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 9, + "m_KeywordIndices": [ + 5 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 10, + "m_KeywordIndices": [ + 5, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 11, + "m_KeywordIndices": [ + 5, + 7 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 12, + "m_KeywordIndices": [ + 5, + 7, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 13, + "m_KeywordIndices": [ + 5, + 6 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 14, + "m_KeywordIndices": [ + 5, + 6, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 15, + "m_KeywordIndices": [ + 5, + 6, + 7 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 16, + "m_KeywordIndices": [ + 5, + 6, + 7, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 17, + "m_KeywordIndices": [ + 4 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 18, + "m_KeywordIndices": [ + 4, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 19, + "m_KeywordIndices": [ + 4, + 7 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 20, + "m_KeywordIndices": [ + 4, + 7, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 21, + "m_KeywordIndices": [ + 4, + 6 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 22, + "m_KeywordIndices": [ + 4, + 6, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 23, + "m_KeywordIndices": [ + 4, + 6, + 7 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 24, + "m_KeywordIndices": [ + 4, + 6, + 7, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 25, + "m_KeywordIndices": [ + 4, + 5 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 26, + "m_KeywordIndices": [ + 4, + 5, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 27, + "m_KeywordIndices": [ + 4, + 5, + 7 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 28, + "m_KeywordIndices": [ + 4, + 5, + 7, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 29, + "m_KeywordIndices": [ + 4, + 5, + 6 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 30, + "m_KeywordIndices": [ + 4, + 5, + 6, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 31, + "m_KeywordIndices": [ + 4, + 5, + 6, + 7 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 32, + "m_KeywordIndices": [ + 4, + 5, + 6, + 7, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + } + ] + ], + "m_ParameterBlobIndices": [ + [], + [], + [], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + ], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [ + { + "m_NameIndex": 0, + "m_MatrixParams": [ + { + "m_NameIndex": 3, + "m_Index": 240, + "m_ArraySize": 0, + "m_Type": 0, + "m_RowCount": 4 + } + ], + "m_VectorParams": [], + "m_StructParams": [], + "m_Size": 544, + "m_IsPartialCB": true + }, + { + "m_NameIndex": 1, + "m_MatrixParams": [], + "m_VectorParams": [ + { + "m_NameIndex": 4, + "m_Index": 80, + "m_ArraySize": 0, + "m_Type": 0, + "m_Dim": 4 + } + ], + "m_StructParams": [], + "m_Size": 144, + "m_IsPartialCB": true + }, + { + "m_NameIndex": 2, + "m_MatrixParams": [ + { + "m_NameIndex": 5, + "m_Index": 0, + "m_ArraySize": 0, + "m_Type": 0, + "m_RowCount": 4 + } + ], + "m_VectorParams": [], + "m_StructParams": [], + "m_Size": 176, + "m_IsPartialCB": true + } + ], + "m_ConstantBufferBindings": [ + { + "m_NameIndex": 0, + "m_Index": 0, + "m_ArraySize": 1 + }, + { + "m_NameIndex": 1, + "m_Index": 1, + "m_ArraySize": 1 + }, + { + "m_NameIndex": 2, + "m_Index": 2, + "m_ArraySize": 1 + } + ], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ] + }, + "progFragment": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [ + [], + [], + [], + [ + { + "m_BlobIndex": 50, + "m_KeywordIndices": [], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 51, + "m_KeywordIndices": [ + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 52, + "m_KeywordIndices": [ + 7 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 53, + "m_KeywordIndices": [ + 7, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 54, + "m_KeywordIndices": [ + 6 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 55, + "m_KeywordIndices": [ + 6, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 56, + "m_KeywordIndices": [ + 6, + 7 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 57, + "m_KeywordIndices": [ + 6, + 7, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 58, + "m_KeywordIndices": [ + 5 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 59, + "m_KeywordIndices": [ + 5, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 60, + "m_KeywordIndices": [ + 5, + 7 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 61, + "m_KeywordIndices": [ + 5, + 7, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 62, + "m_KeywordIndices": [ + 5, + 6 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 63, + "m_KeywordIndices": [ + 5, + 6, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 64, + "m_KeywordIndices": [ + 5, + 6, + 7 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 65, + "m_KeywordIndices": [ + 5, + 6, + 7, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 66, + "m_KeywordIndices": [ + 4 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 67, + "m_KeywordIndices": [ + 4, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 68, + "m_KeywordIndices": [ + 4, + 7 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 69, + "m_KeywordIndices": [ + 4, + 7, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 70, + "m_KeywordIndices": [ + 4, + 6 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 71, + "m_KeywordIndices": [ + 4, + 6, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 72, + "m_KeywordIndices": [ + 4, + 6, + 7 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 73, + "m_KeywordIndices": [ + 4, + 6, + 7, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 74, + "m_KeywordIndices": [ + 4, + 5 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 75, + "m_KeywordIndices": [ + 4, + 5, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 76, + "m_KeywordIndices": [ + 4, + 5, + 7 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 77, + "m_KeywordIndices": [ + 4, + 5, + 7, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 78, + "m_KeywordIndices": [ + 4, + 5, + 6 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 79, + "m_KeywordIndices": [ + 4, + 5, + 6, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 80, + "m_KeywordIndices": [ + 4, + 5, + 6, + 7 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 81, + "m_KeywordIndices": [ + 4, + 5, + 6, + 7, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + } + ] + ], + "m_ParameterBlobIndices": [ + [], + [], + [], + [ + 33, + 34, + 35, + 35, + 33, + 36, + 37, + 37, + 33, + 38, + 39, + 39, + 33, + 40, + 41, + 41, + 33, + 42, + 43, + 43, + 33, + 44, + 45, + 45, + 33, + 46, + 47, + 47, + 33, + 48, + 49, + 49 + ] + ], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ] + }, + "progGeometry": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progHull": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progDomain": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progRayTracing": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "m_HasInstancingVariant": false, + "m_HasProceduralInstancingVariant": false, + "m_UseName": "", + "m_Name": "", + "m_TextureName": "", + "m_Tags": { + "tags": [] + } + }, + { + "m_NameIndices": [ + { + "Key": "$Globals", + "Value": 0 + }, + { + "Key": "UnityPerCamera", + "Value": 1 + }, + { + "Key": "UnityPerDraw", + "Value": 2 + }, + { + "Key": "_ProjectionParams", + "Value": 4 + }, + { + "Key": "_WorldViewProj", + "Value": 3 + }, + { + "Key": "unity_ObjectToWorld", + "Value": 5 + } + ], + "m_Type": 0, + "m_State": { + "m_Name": "", + "rtBlend0": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 1.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 1.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend1": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend2": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend3": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend4": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend5": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend6": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend7": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtSeparateBlend": false, + "zClip": { + "val": 1.0, + "name": "" + }, + "zTest": { + "val": 0.0, + "name": "" + }, + "zWrite": { + "val": 0.0, + "name": "" + }, + "culling": { + "val": 1.0, + "name": "" + }, + "conservative": { + "val": 0.0, + "name": "" + }, + "offsetFactor": { + "val": 0.0, + "name": "" + }, + "offsetUnits": { + "val": 0.0, + "name": "" + }, + "alphaToMask": { + "val": 0.0, + "name": "" + }, + "stencilOp": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilOpFront": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilOpBack": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilReadMask": { + "val": 255.0, + "name": "" + }, + "stencilWriteMask": { + "val": 255.0, + "name": "" + }, + "stencilRef": { + "val": 0.0, + "name": "" + }, + "fogStart": { + "val": 0.0, + "name": "unity_FogStart" + }, + "fogEnd": { + "val": 0.0, + "name": "unity_FogEnd" + }, + "fogDensity": { + "val": 0.0, + "name": "unity_FogDensity" + }, + "fogColor": { + "x": { + "val": 0.0, + "name": "" + }, + "y": { + "val": 0.0, + "name": "" + }, + "z": { + "val": 0.0, + "name": "" + }, + "w": { + "val": 0.0, + "name": "" + }, + "name": "unity_FogColor" + }, + "fogMode": -1, + "gpuProgramID": 85808, + "m_Tags": { + "tags": [ + { + "Key": "RenderType", + "Value": "Opaque" + } + ] + }, + "m_LOD": 100, + "lighting": false + }, + "m_ProgramMask": 6, + "progVertex": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [ + [], + [], + [], + [ + { + "m_BlobIndex": 1, + "m_KeywordIndices": [], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 82, + "m_KeywordIndices": [ + 10 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 83, + "m_KeywordIndices": [ + 9 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 84, + "m_KeywordIndices": [ + 9, + 10 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 9, + "m_KeywordIndices": [ + 5 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 85, + "m_KeywordIndices": [ + 5, + 10 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 86, + "m_KeywordIndices": [ + 5, + 9 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 87, + "m_KeywordIndices": [ + 5, + 9, + 10 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 17, + "m_KeywordIndices": [ + 4 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 88, + "m_KeywordIndices": [ + 4, + 10 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 89, + "m_KeywordIndices": [ + 4, + 9 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 90, + "m_KeywordIndices": [ + 4, + 9, + 10 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 25, + "m_KeywordIndices": [ + 4, + 5 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 91, + "m_KeywordIndices": [ + 4, + 5, + 10 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 92, + "m_KeywordIndices": [ + 4, + 5, + 9 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 93, + "m_KeywordIndices": [ + 4, + 5, + 9, + 10 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + } + ] + ], + "m_ParameterBlobIndices": [ + [], + [], + [], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + ], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [ + { + "m_NameIndex": 0, + "m_MatrixParams": [ + { + "m_NameIndex": 3, + "m_Index": 240, + "m_ArraySize": 0, + "m_Type": 0, + "m_RowCount": 4 + } + ], + "m_VectorParams": [], + "m_StructParams": [], + "m_Size": 544, + "m_IsPartialCB": true + }, + { + "m_NameIndex": 1, + "m_MatrixParams": [], + "m_VectorParams": [ + { + "m_NameIndex": 4, + "m_Index": 80, + "m_ArraySize": 0, + "m_Type": 0, + "m_Dim": 4 + } + ], + "m_StructParams": [], + "m_Size": 144, + "m_IsPartialCB": true + }, + { + "m_NameIndex": 2, + "m_MatrixParams": [ + { + "m_NameIndex": 5, + "m_Index": 0, + "m_ArraySize": 0, + "m_Type": 0, + "m_RowCount": 4 + } + ], + "m_VectorParams": [], + "m_StructParams": [], + "m_Size": 176, + "m_IsPartialCB": true + } + ], + "m_ConstantBufferBindings": [ + { + "m_NameIndex": 0, + "m_Index": 0, + "m_ArraySize": 1 + }, + { + "m_NameIndex": 1, + "m_Index": 1, + "m_ArraySize": 1 + }, + { + "m_NameIndex": 2, + "m_Index": 2, + "m_ArraySize": 1 + } + ], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [ + 0, + 1, + 2, + 3, + 4, + 5, + 9, + 10 + ] + }, + "progFragment": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [ + [], + [], + [], + [ + { + "m_BlobIndex": 50, + "m_KeywordIndices": [], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 102, + "m_KeywordIndices": [ + 10 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 103, + "m_KeywordIndices": [ + 9 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 104, + "m_KeywordIndices": [ + 9, + 10 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 58, + "m_KeywordIndices": [ + 5 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 105, + "m_KeywordIndices": [ + 5, + 10 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 106, + "m_KeywordIndices": [ + 5, + 9 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 107, + "m_KeywordIndices": [ + 5, + 9, + 10 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 66, + "m_KeywordIndices": [ + 4 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 108, + "m_KeywordIndices": [ + 4, + 10 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 109, + "m_KeywordIndices": [ + 4, + 9 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 110, + "m_KeywordIndices": [ + 4, + 9, + 10 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 74, + "m_KeywordIndices": [ + 4, + 5 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 111, + "m_KeywordIndices": [ + 4, + 5, + 10 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 112, + "m_KeywordIndices": [ + 4, + 5, + 9 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 113, + "m_KeywordIndices": [ + 4, + 5, + 9, + 10 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + } + ] + ], + "m_ParameterBlobIndices": [ + [], + [], + [], + [ + 33, + 94, + 33, + 95, + 33, + 96, + 33, + 97, + 33, + 98, + 33, + 99, + 33, + 100, + 33, + 101 + ] + ], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [ + 0, + 1, + 2, + 3, + 4, + 5, + 9, + 10 + ] + }, + "progGeometry": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progHull": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progDomain": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progRayTracing": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "m_HasInstancingVariant": false, + "m_HasProceduralInstancingVariant": false, + "m_UseName": "", + "m_Name": "", + "m_TextureName": "", + "m_Tags": { + "tags": [] + } + }, + { + "m_NameIndices": [ + { + "Key": "$Globals", + "Value": 0 + }, + { + "Key": "UnityPerCamera", + "Value": 1 + }, + { + "Key": "UnityPerDraw", + "Value": 2 + }, + { + "Key": "_ProjectionParams", + "Value": 4 + }, + { + "Key": "_WorldViewProj", + "Value": 3 + }, + { + "Key": "unity_ObjectToWorld", + "Value": 5 + } + ], + "m_Type": 0, + "m_State": { + "m_Name": "", + "rtBlend0": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 1.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 1.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend1": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend2": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend3": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend4": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend5": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend6": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend7": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtSeparateBlend": false, + "zClip": { + "val": 1.0, + "name": "" + }, + "zTest": { + "val": 0.0, + "name": "_ZTest" + }, + "zWrite": { + "val": 0.0, + "name": "" + }, + "culling": { + "val": 2.0, + "name": "" + }, + "conservative": { + "val": 0.0, + "name": "" + }, + "offsetFactor": { + "val": 0.0, + "name": "" + }, + "offsetUnits": { + "val": 0.0, + "name": "" + }, + "alphaToMask": { + "val": 0.0, + "name": "" + }, + "stencilOp": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilOpFront": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilOpBack": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilReadMask": { + "val": 255.0, + "name": "" + }, + "stencilWriteMask": { + "val": 255.0, + "name": "" + }, + "stencilRef": { + "val": 0.0, + "name": "" + }, + "fogStart": { + "val": 0.0, + "name": "unity_FogStart" + }, + "fogEnd": { + "val": 0.0, + "name": "unity_FogEnd" + }, + "fogDensity": { + "val": 0.0, + "name": "unity_FogDensity" + }, + "fogColor": { + "x": { + "val": 0.0, + "name": "" + }, + "y": { + "val": 0.0, + "name": "" + }, + "z": { + "val": 0.0, + "name": "" + }, + "w": { + "val": 0.0, + "name": "" + }, + "name": "unity_FogColor" + }, + "fogMode": -1, + "gpuProgramID": 136503, + "m_Tags": { + "tags": [ + { + "Key": "RenderType", + "Value": "Opaque" + } + ] + }, + "m_LOD": 100, + "lighting": false + }, + "m_ProgramMask": 6, + "progVertex": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [ + [], + [], + [], + [ + { + "m_BlobIndex": 1, + "m_KeywordIndices": [], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 2, + "m_KeywordIndices": [ + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 3, + "m_KeywordIndices": [ + 7 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 4, + "m_KeywordIndices": [ + 7, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 9, + "m_KeywordIndices": [ + 5 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 10, + "m_KeywordIndices": [ + 5, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 11, + "m_KeywordIndices": [ + 5, + 7 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 12, + "m_KeywordIndices": [ + 5, + 7, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 5, + "m_KeywordIndices": [ + 6 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 6, + "m_KeywordIndices": [ + 6, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 7, + "m_KeywordIndices": [ + 6, + 7 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 8, + "m_KeywordIndices": [ + 6, + 7, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 13, + "m_KeywordIndices": [ + 5, + 6 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 14, + "m_KeywordIndices": [ + 5, + 6, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 15, + "m_KeywordIndices": [ + 5, + 6, + 7 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 16, + "m_KeywordIndices": [ + 5, + 6, + 7, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 17, + "m_KeywordIndices": [ + 4 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 18, + "m_KeywordIndices": [ + 4, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 19, + "m_KeywordIndices": [ + 4, + 7 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 20, + "m_KeywordIndices": [ + 4, + 7, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 25, + "m_KeywordIndices": [ + 4, + 5 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 26, + "m_KeywordIndices": [ + 4, + 5, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 27, + "m_KeywordIndices": [ + 4, + 5, + 7 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 28, + "m_KeywordIndices": [ + 4, + 5, + 7, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 21, + "m_KeywordIndices": [ + 4, + 6 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 22, + "m_KeywordIndices": [ + 4, + 6, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 23, + "m_KeywordIndices": [ + 4, + 6, + 7 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 24, + "m_KeywordIndices": [ + 4, + 6, + 7, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 29, + "m_KeywordIndices": [ + 4, + 5, + 6 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 30, + "m_KeywordIndices": [ + 4, + 5, + 6, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 31, + "m_KeywordIndices": [ + 4, + 5, + 6, + 7 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 32, + "m_KeywordIndices": [ + 4, + 5, + 6, + 7, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + } + ] + ], + "m_ParameterBlobIndices": [ + [], + [], + [], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + ], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [ + { + "m_NameIndex": 0, + "m_MatrixParams": [ + { + "m_NameIndex": 3, + "m_Index": 240, + "m_ArraySize": 0, + "m_Type": 0, + "m_RowCount": 4 + } + ], + "m_VectorParams": [], + "m_StructParams": [], + "m_Size": 544, + "m_IsPartialCB": true + }, + { + "m_NameIndex": 1, + "m_MatrixParams": [], + "m_VectorParams": [ + { + "m_NameIndex": 4, + "m_Index": 80, + "m_ArraySize": 0, + "m_Type": 0, + "m_Dim": 4 + } + ], + "m_StructParams": [], + "m_Size": 144, + "m_IsPartialCB": true + }, + { + "m_NameIndex": 2, + "m_MatrixParams": [ + { + "m_NameIndex": 5, + "m_Index": 0, + "m_ArraySize": 0, + "m_Type": 0, + "m_RowCount": 4 + } + ], + "m_VectorParams": [], + "m_StructParams": [], + "m_Size": 176, + "m_IsPartialCB": true + } + ], + "m_ConstantBufferBindings": [ + { + "m_NameIndex": 0, + "m_Index": 0, + "m_ArraySize": 1 + }, + { + "m_NameIndex": 1, + "m_Index": 1, + "m_ArraySize": 1 + }, + { + "m_NameIndex": 2, + "m_Index": 2, + "m_ArraySize": 1 + } + ], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ] + }, + "progFragment": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [ + [], + [], + [], + [ + { + "m_BlobIndex": 50, + "m_KeywordIndices": [], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 114, + "m_KeywordIndices": [ + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 115, + "m_KeywordIndices": [ + 7 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 116, + "m_KeywordIndices": [ + 7, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 58, + "m_KeywordIndices": [ + 5 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 117, + "m_KeywordIndices": [ + 5, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 118, + "m_KeywordIndices": [ + 5, + 7 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 119, + "m_KeywordIndices": [ + 5, + 7, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 54, + "m_KeywordIndices": [ + 6 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 120, + "m_KeywordIndices": [ + 6, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 121, + "m_KeywordIndices": [ + 6, + 7 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 122, + "m_KeywordIndices": [ + 6, + 7, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 62, + "m_KeywordIndices": [ + 5, + 6 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 123, + "m_KeywordIndices": [ + 5, + 6, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 124, + "m_KeywordIndices": [ + 5, + 6, + 7 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 125, + "m_KeywordIndices": [ + 5, + 6, + 7, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 66, + "m_KeywordIndices": [ + 4 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 126, + "m_KeywordIndices": [ + 4, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 127, + "m_KeywordIndices": [ + 4, + 7 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 128, + "m_KeywordIndices": [ + 4, + 7, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 74, + "m_KeywordIndices": [ + 4, + 5 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 129, + "m_KeywordIndices": [ + 4, + 5, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 130, + "m_KeywordIndices": [ + 4, + 5, + 7 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 131, + "m_KeywordIndices": [ + 4, + 5, + 7, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 70, + "m_KeywordIndices": [ + 4, + 6 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 132, + "m_KeywordIndices": [ + 4, + 6, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 133, + "m_KeywordIndices": [ + 4, + 6, + 7 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 134, + "m_KeywordIndices": [ + 4, + 6, + 7, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 78, + "m_KeywordIndices": [ + 4, + 5, + 6 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 135, + "m_KeywordIndices": [ + 4, + 5, + 6, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 136, + "m_KeywordIndices": [ + 4, + 5, + 6, + 7 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 137, + "m_KeywordIndices": [ + 4, + 5, + 6, + 7, + 8 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + } + ] + ], + "m_ParameterBlobIndices": [ + [], + [], + [], + [ + 33, + 34, + 35, + 35, + 33, + 38, + 39, + 39, + 33, + 36, + 37, + 37, + 33, + 40, + 41, + 41, + 33, + 42, + 43, + 43, + 33, + 46, + 47, + 47, + 33, + 44, + 45, + 45, + 33, + 48, + 49, + 49 + ] + ], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ] + }, + "progGeometry": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progHull": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progDomain": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progRayTracing": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "m_HasInstancingVariant": false, + "m_HasProceduralInstancingVariant": false, + "m_UseName": "", + "m_Name": "", + "m_TextureName": "", + "m_Tags": { + "tags": [] + } + }, + { + "m_NameIndices": [ + { + "Key": "$Globals", + "Value": 0 + }, + { + "Key": "UnityPerCamera", + "Value": 1 + }, + { + "Key": "UnityPerDraw", + "Value": 2 + }, + { + "Key": "_ProjectionParams", + "Value": 4 + }, + { + "Key": "_WorldViewProj", + "Value": 3 + }, + { + "Key": "unity_ObjectToWorld", + "Value": 5 + } + ], + "m_Type": 0, + "m_State": { + "m_Name": "", + "rtBlend0": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 1.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 1.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend1": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend2": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend3": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend4": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend5": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend6": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend7": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtSeparateBlend": false, + "zClip": { + "val": 1.0, + "name": "" + }, + "zTest": { + "val": 0.0, + "name": "_ZTest" + }, + "zWrite": { + "val": 0.0, + "name": "" + }, + "culling": { + "val": 2.0, + "name": "" + }, + "conservative": { + "val": 0.0, + "name": "" + }, + "offsetFactor": { + "val": 0.0, + "name": "" + }, + "offsetUnits": { + "val": 0.0, + "name": "" + }, + "alphaToMask": { + "val": 0.0, + "name": "" + }, + "stencilOp": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilOpFront": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilOpBack": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilReadMask": { + "val": 255.0, + "name": "" + }, + "stencilWriteMask": { + "val": 255.0, + "name": "" + }, + "stencilRef": { + "val": 0.0, + "name": "" + }, + "fogStart": { + "val": 0.0, + "name": "unity_FogStart" + }, + "fogEnd": { + "val": 0.0, + "name": "unity_FogEnd" + }, + "fogDensity": { + "val": 0.0, + "name": "unity_FogDensity" + }, + "fogColor": { + "x": { + "val": 0.0, + "name": "" + }, + "y": { + "val": 0.0, + "name": "" + }, + "z": { + "val": 0.0, + "name": "" + }, + "w": { + "val": 0.0, + "name": "" + }, + "name": "unity_FogColor" + }, + "fogMode": -1, + "gpuProgramID": 237727, + "m_Tags": { + "tags": [ + { + "Key": "RenderType", + "Value": "Opaque" + } + ] + }, + "m_LOD": 100, + "lighting": false + }, + "m_ProgramMask": 6, + "progVertex": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [ + [], + [], + [], + [ + { + "m_BlobIndex": 1, + "m_KeywordIndices": [], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 82, + "m_KeywordIndices": [ + 10 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 9, + "m_KeywordIndices": [ + 5 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 85, + "m_KeywordIndices": [ + 5, + 10 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 83, + "m_KeywordIndices": [ + 9 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 84, + "m_KeywordIndices": [ + 9, + 10 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 86, + "m_KeywordIndices": [ + 5, + 9 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 87, + "m_KeywordIndices": [ + 5, + 9, + 10 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 17, + "m_KeywordIndices": [ + 4 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 88, + "m_KeywordIndices": [ + 4, + 10 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 25, + "m_KeywordIndices": [ + 4, + 5 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 91, + "m_KeywordIndices": [ + 4, + 5, + 10 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 89, + "m_KeywordIndices": [ + 4, + 9 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 90, + "m_KeywordIndices": [ + 4, + 9, + 10 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 92, + "m_KeywordIndices": [ + 4, + 5, + 9 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 93, + "m_KeywordIndices": [ + 4, + 5, + 9, + 10 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + } + ] + ], + "m_ParameterBlobIndices": [ + [], + [], + [], + [ + 138, + 138, + 138, + 138, + 138, + 138, + 138, + 138, + 138, + 138, + 138, + 138, + 138, + 138, + 138, + 138 + ] + ], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [ + { + "m_NameIndex": 0, + "m_MatrixParams": [ + { + "m_NameIndex": 3, + "m_Index": 240, + "m_ArraySize": 0, + "m_Type": 0, + "m_RowCount": 4 + } + ], + "m_VectorParams": [], + "m_StructParams": [], + "m_Size": 592, + "m_IsPartialCB": true + }, + { + "m_NameIndex": 1, + "m_MatrixParams": [], + "m_VectorParams": [ + { + "m_NameIndex": 4, + "m_Index": 80, + "m_ArraySize": 0, + "m_Type": 0, + "m_Dim": 4 + } + ], + "m_StructParams": [], + "m_Size": 144, + "m_IsPartialCB": true + }, + { + "m_NameIndex": 2, + "m_MatrixParams": [ + { + "m_NameIndex": 5, + "m_Index": 0, + "m_ArraySize": 0, + "m_Type": 0, + "m_RowCount": 4 + } + ], + "m_VectorParams": [], + "m_StructParams": [], + "m_Size": 176, + "m_IsPartialCB": true + } + ], + "m_ConstantBufferBindings": [ + { + "m_NameIndex": 0, + "m_Index": 0, + "m_ArraySize": 1 + }, + { + "m_NameIndex": 1, + "m_Index": 1, + "m_ArraySize": 1 + }, + { + "m_NameIndex": 2, + "m_Index": 2, + "m_ArraySize": 1 + } + ], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [ + 0, + 1, + 2, + 3, + 4, + 5, + 9, + 10 + ] + }, + "progFragment": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [ + [], + [], + [], + [ + { + "m_BlobIndex": 50, + "m_KeywordIndices": [], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 147, + "m_KeywordIndices": [ + 10 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 58, + "m_KeywordIndices": [ + 5 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 148, + "m_KeywordIndices": [ + 5, + 10 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 103, + "m_KeywordIndices": [ + 9 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 149, + "m_KeywordIndices": [ + 9, + 10 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 106, + "m_KeywordIndices": [ + 5, + 9 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 150, + "m_KeywordIndices": [ + 5, + 9, + 10 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 66, + "m_KeywordIndices": [ + 4 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 151, + "m_KeywordIndices": [ + 4, + 10 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 74, + "m_KeywordIndices": [ + 4, + 5 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 152, + "m_KeywordIndices": [ + 4, + 5, + 10 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 109, + "m_KeywordIndices": [ + 4, + 9 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 153, + "m_KeywordIndices": [ + 4, + 9, + 10 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 112, + "m_KeywordIndices": [ + 4, + 5, + 9 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 154, + "m_KeywordIndices": [ + 4, + 5, + 9, + 10 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + } + ] + ], + "m_ParameterBlobIndices": [ + [], + [], + [], + [ + 33, + 139, + 33, + 140, + 33, + 141, + 33, + 142, + 33, + 143, + 33, + 144, + 33, + 145, + 33, + 146 + ] + ], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [ + 0, + 1, + 2, + 3, + 4, + 5, + 9, + 10 + ] + }, + "progGeometry": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progHull": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progDomain": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progRayTracing": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "m_HasInstancingVariant": false, + "m_HasProceduralInstancingVariant": false, + "m_UseName": "", + "m_Name": "", + "m_TextureName": "", + "m_Tags": { + "tags": [] + } + }, + { + "m_NameIndices": [ + { + "Key": "$Globals", + "Value": 0 + }, + { + "Key": "UnityPerCamera", + "Value": 7 + }, + { + "Key": "UnityPerDraw", + "Value": 1 + }, + { + "Key": "UnityPerFrame", + "Value": 2 + }, + { + "Key": "_CameraDepthTexture", + "Value": 6 + }, + { + "Key": "_FrustumCorners", + "Value": 3 + }, + { + "Key": "_VolumetricLight", + "Value": 8 + }, + { + "Key": "_ZBufferParams", + "Value": 9 + }, + { + "Key": "unity_MatrixVP", + "Value": 5 + }, + { + "Key": "unity_ObjectToWorld", + "Value": 4 + } + ], + "m_Type": 0, + "m_State": { + "m_Name": "", + "rtBlend0": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 1.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend1": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend2": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend3": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend4": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend5": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend6": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtBlend7": { + "srcBlend": { + "val": 1.0, + "name": "" + }, + "destBlend": { + "val": 0.0, + "name": "" + }, + "srcBlendAlpha": { + "val": 1.0, + "name": "" + }, + "destBlendAlpha": { + "val": 0.0, + "name": "" + }, + "blendOp": { + "val": 0.0, + "name": "" + }, + "blendOpAlpha": { + "val": 0.0, + "name": "" + }, + "colMask": { + "val": 15.0, + "name": "" + } + }, + "rtSeparateBlend": false, + "zClip": { + "val": 1.0, + "name": "" + }, + "zTest": { + "val": 0.0, + "name": "" + }, + "zWrite": { + "val": 0.0, + "name": "" + }, + "culling": { + "val": 0.0, + "name": "" + }, + "conservative": { + "val": 0.0, + "name": "" + }, + "offsetFactor": { + "val": 0.0, + "name": "" + }, + "offsetUnits": { + "val": 0.0, + "name": "" + }, + "alphaToMask": { + "val": 0.0, + "name": "" + }, + "stencilOp": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilOpFront": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilOpBack": { + "pass": { + "val": 0.0, + "name": "" + }, + "fail": { + "val": 0.0, + "name": "" + }, + "zFail": { + "val": 0.0, + "name": "" + }, + "comp": { + "val": 8.0, + "name": "" + } + }, + "stencilReadMask": { + "val": 255.0, + "name": "" + }, + "stencilWriteMask": { + "val": 255.0, + "name": "" + }, + "stencilRef": { + "val": 0.0, + "name": "" + }, + "fogStart": { + "val": 0.0, + "name": "unity_FogStart" + }, + "fogEnd": { + "val": 0.0, + "name": "unity_FogEnd" + }, + "fogDensity": { + "val": 0.0, + "name": "unity_FogDensity" + }, + "fogColor": { + "x": { + "val": 0.0, + "name": "" + }, + "y": { + "val": 0.0, + "name": "" + }, + "z": { + "val": 0.0, + "name": "" + }, + "w": { + "val": 0.0, + "name": "" + }, + "name": "unity_FogColor" + }, + "fogMode": -1, + "gpuProgramID": 288407, + "m_Tags": { + "tags": [ + { + "Key": "RenderType", + "Value": "Opaque" + } + ] + }, + "m_LOD": 100, + "lighting": false + }, + "m_ProgramMask": 6, + "progVertex": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [ + [], + [], + [], + [ + { + "m_BlobIndex": 156, + "m_KeywordIndices": [], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 157, + "m_KeywordIndices": [ + 12 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 158, + "m_KeywordIndices": [ + 11 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 159, + "m_KeywordIndices": [ + 11, + 12 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 160, + "m_KeywordIndices": [ + 9 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 161, + "m_KeywordIndices": [ + 9, + 12 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 162, + "m_KeywordIndices": [ + 9, + 11 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 163, + "m_KeywordIndices": [ + 9, + 11, + 12 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 164, + "m_KeywordIndices": [ + 5 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 165, + "m_KeywordIndices": [ + 5, + 12 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 166, + "m_KeywordIndices": [ + 5, + 11 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 167, + "m_KeywordIndices": [ + 5, + 11, + 12 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 168, + "m_KeywordIndices": [ + 5, + 9 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 169, + "m_KeywordIndices": [ + 5, + 9, + 12 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 170, + "m_KeywordIndices": [ + 5, + 9, + 11 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 171, + "m_KeywordIndices": [ + 5, + 9, + 11, + 12 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 172, + "m_KeywordIndices": [ + 4 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 173, + "m_KeywordIndices": [ + 4, + 12 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 174, + "m_KeywordIndices": [ + 4, + 11 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 175, + "m_KeywordIndices": [ + 4, + 11, + 12 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 176, + "m_KeywordIndices": [ + 4, + 9 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 177, + "m_KeywordIndices": [ + 4, + 9, + 12 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 178, + "m_KeywordIndices": [ + 4, + 9, + 11 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 179, + "m_KeywordIndices": [ + 4, + 9, + 11, + 12 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 180, + "m_KeywordIndices": [ + 4, + 5 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 181, + "m_KeywordIndices": [ + 4, + 5, + 12 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 182, + "m_KeywordIndices": [ + 4, + 5, + 11 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 183, + "m_KeywordIndices": [ + 4, + 5, + 11, + 12 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 184, + "m_KeywordIndices": [ + 4, + 5, + 9 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 185, + "m_KeywordIndices": [ + 4, + 5, + 9, + 12 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 186, + "m_KeywordIndices": [ + 4, + 5, + 9, + 11 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + }, + { + "m_BlobIndex": 187, + "m_KeywordIndices": [ + 4, + 5, + 9, + 11, + 12 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 15 + } + ] + ], + "m_ParameterBlobIndices": [ + [], + [], + [], + [ + 155, + 155, + 155, + 155, + 155, + 155, + 155, + 155, + 155, + 155, + 155, + 155, + 155, + 155, + 155, + 155, + 155, + 155, + 155, + 155, + 155, + 155, + 155, + 155, + 155, + 155, + 155, + 155, + 155, + 155, + 155, + 155 + ] + ], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [ + { + "m_NameIndex": 0, + "m_MatrixParams": [], + "m_VectorParams": [ + { + "m_NameIndex": 3, + "m_Index": 176, + "m_ArraySize": 4, + "m_Type": 0, + "m_Dim": 4 + } + ], + "m_StructParams": [], + "m_Size": 544, + "m_IsPartialCB": true + }, + { + "m_NameIndex": 1, + "m_MatrixParams": [ + { + "m_NameIndex": 4, + "m_Index": 0, + "m_ArraySize": 0, + "m_Type": 0, + "m_RowCount": 4 + } + ], + "m_VectorParams": [], + "m_StructParams": [], + "m_Size": 176, + "m_IsPartialCB": true + }, + { + "m_NameIndex": 2, + "m_MatrixParams": [ + { + "m_NameIndex": 5, + "m_Index": 272, + "m_ArraySize": 0, + "m_Type": 0, + "m_RowCount": 4 + } + ], + "m_VectorParams": [], + "m_StructParams": [], + "m_Size": 368, + "m_IsPartialCB": true + } + ], + "m_ConstantBufferBindings": [ + { + "m_NameIndex": 0, + "m_Index": 0, + "m_ArraySize": 1 + }, + { + "m_NameIndex": 1, + "m_Index": 1, + "m_ArraySize": 1 + }, + { + "m_NameIndex": 2, + "m_Index": 2, + "m_ArraySize": 1 + } + ], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [ + 0, + 1, + 2, + 3, + 4, + 5, + 9, + 11, + 12 + ] + }, + "progFragment": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [ + [], + [], + [], + [ + { + "m_BlobIndex": 197, + "m_KeywordIndices": [], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 198, + "m_KeywordIndices": [ + 12 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 199, + "m_KeywordIndices": [ + 11 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 200, + "m_KeywordIndices": [ + 11, + 12 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 201, + "m_KeywordIndices": [ + 9 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 202, + "m_KeywordIndices": [ + 9, + 12 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 203, + "m_KeywordIndices": [ + 9, + 11 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 204, + "m_KeywordIndices": [ + 9, + 11, + 12 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 205, + "m_KeywordIndices": [ + 5 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 206, + "m_KeywordIndices": [ + 5, + 12 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 207, + "m_KeywordIndices": [ + 5, + 11 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 208, + "m_KeywordIndices": [ + 5, + 11, + 12 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 209, + "m_KeywordIndices": [ + 5, + 9 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 210, + "m_KeywordIndices": [ + 5, + 9, + 12 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 211, + "m_KeywordIndices": [ + 5, + 9, + 11 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 212, + "m_KeywordIndices": [ + 5, + 9, + 11, + 12 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 213, + "m_KeywordIndices": [ + 4 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 214, + "m_KeywordIndices": [ + 4, + 12 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 215, + "m_KeywordIndices": [ + 4, + 11 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 216, + "m_KeywordIndices": [ + 4, + 11, + 12 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 217, + "m_KeywordIndices": [ + 4, + 9 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 218, + "m_KeywordIndices": [ + 4, + 9, + 12 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 219, + "m_KeywordIndices": [ + 4, + 9, + 11 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 220, + "m_KeywordIndices": [ + 4, + 9, + 11, + 12 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 221, + "m_KeywordIndices": [ + 4, + 5 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 222, + "m_KeywordIndices": [ + 4, + 5, + 12 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 223, + "m_KeywordIndices": [ + 4, + 5, + 11 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 224, + "m_KeywordIndices": [ + 4, + 5, + 11, + 12 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 225, + "m_KeywordIndices": [ + 4, + 5, + 9 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 226, + "m_KeywordIndices": [ + 4, + 5, + 9, + 12 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 227, + "m_KeywordIndices": [ + 4, + 5, + 9, + 11 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + }, + { + "m_BlobIndex": 228, + "m_KeywordIndices": [ + 4, + 5, + 9, + 11, + 12 + ], + "m_ShaderRequirements": 4075, + "m_GpuProgramType": 17 + } + ] + ], + "m_ParameterBlobIndices": [ + [], + [], + [], + [ + 188, + 189, + 189, + 189, + 188, + 190, + 190, + 190, + 188, + 191, + 191, + 191, + 188, + 192, + 192, + 192, + 188, + 193, + 193, + 193, + 188, + 194, + 194, + 194, + 188, + 195, + 195, + 195, + 188, + 196, + 196, + 196 + ] + ], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [ + { + "m_NameIndex": 6, + "m_Index": 0, + "m_SamplerIndex": 0, + "m_MultiSampled": false, + "m_Dim": 2 + } + ], + "m_BufferParams": [], + "m_ConstantBuffers": [ + { + "m_NameIndex": 0, + "m_MatrixParams": [], + "m_VectorParams": [ + { + "m_NameIndex": 8, + "m_Index": 448, + "m_ArraySize": 0, + "m_Type": 0, + "m_Dim": 4 + } + ], + "m_StructParams": [], + "m_Size": 544, + "m_IsPartialCB": true + }, + { + "m_NameIndex": 7, + "m_MatrixParams": [], + "m_VectorParams": [ + { + "m_NameIndex": 9, + "m_Index": 112, + "m_ArraySize": 0, + "m_Type": 0, + "m_Dim": 4 + } + ], + "m_StructParams": [], + "m_Size": 144, + "m_IsPartialCB": true + } + ], + "m_ConstantBufferBindings": [ + { + "m_NameIndex": 0, + "m_Index": 0, + "m_ArraySize": 1 + }, + { + "m_NameIndex": 7, + "m_Index": 1, + "m_ArraySize": 1 + } + ], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [ + 0, + 1, + 2, + 3, + 4, + 5, + 9, + 11, + 12 + ] + }, + "progGeometry": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progHull": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progDomain": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "progRayTracing": { + "m_SubPrograms": [], + "m_PlayerSubPrograms": [], + "m_ParameterBlobIndices": [], + "m_CommonParameters": { + "m_VectorParams": [], + "m_MatrixParams": [], + "m_TextureParams": [], + "m_BufferParams": [], + "m_ConstantBuffers": [], + "m_ConstantBufferBindings": [], + "m_UAVParams": [], + "m_Samplers": [] + }, + "m_SerializedKeywordStateMask": [] + }, + "m_HasInstancingVariant": false, + "m_HasProceduralInstancingVariant": false, + "m_UseName": "", + "m_Name": "", + "m_TextureName": "", + "m_Tags": { + "tags": [] + } + } + ], + "m_Tags": { + "tags": [ + { + "Key": "RenderType", + "Value": "Opaque" + } + ] + }, + "m_LOD": 100 + } + ], + "m_KeywordNames": [ + "STEREO_INSTANCING_ON", + "UNITY_SINGLE_PASS_STEREO", + "STEREO_MULTIVIEW_ON", + "STEREO_CUBEMAP_RENDER_ON", + "HEIGHT_FOG", + "NOISE", + "SHADOWS_CUBE", + "POINT_COOKIE", + "POINT", + "SHADOWS_DEPTH", + "SPOT", + "DIRECTIONAL_COOKIE", + "DIRECTIONAL" + ], + "m_KeywordFlags": "AAAAAAAAAAAAAAAAAA==", + "m_Name": "Sandbox/VolumetricLight", + "m_CustomEditorName": "", + "m_FallbackName": "", + "m_Dependencies": [], + "m_CustomEditorForRenderPipelines": [], + "m_DisableNoSubshadersMessage": false + }, + "platforms": [ + 4 + ], + "offsets": [ + [ + 0 + ] + ], + "compressedLengths": [ + [ + 20661 + ] + ], + "decompressedLengths": [ + [ + 395752 + ] + ], + "compressedBlob": "ouUAAADACgAAcAABAGIwCwAAZAMMAFOUDgAAcAwAUwQSAAB0DABTeBUAAIAMACb4GBgAJmwcGABT7B8AAIQMAFNwIwAAkAwAJgAnVABUcCoAAHwwABYtPABUbDEAAIxgABY0GAAmeDgYACYEPFQAU5Q/AACcDAAmMEOQACakRjwAJiRKkAAmqE08ACY4URgAJrxUGABTTFgAAJQMAFPgWwAAoAwAJoBfVAAmAGOQACaMZjwAJhxqkAAmuG0YACZIcRgAJuR0VABUhHgAAKzAAEN8AAAcXwBiTHwAAEwCDABTmH4AAJwMAFQ0gQAATJAAFoRUAFMciAAA6CQAUwSLAAA4GABUPI4AAOgIAVKSAAA4BAwAU1yWAAB0MABT0JgAAMQMACaUm0QBUwifAADEDABTzKIAABAMAFTcpQAAYGAAQ6kAABBUAFNMrQAAYAwAYqyxAABgAQwAYwyzAADECGwAU7sAAKgJ1AFDxQAAtAwAUyzPAABwMABjnNAAAAgLyAFD2wAA7AwAU5DnAAD4DABTiPMAAGwwAFP09AAA4EgAY9T+AADYCoQAQwkBAOQMAFSQFAEAfJAAUxYBACQMdAFTIgEACA0QAkMvAQAUDAA1TDwBkABTvD0BAGBgAFMcRwEAWFQAU3RRAQBkDABT2FsBAIAwAFNYXQEApKgAU/xoAQCcbABTmHUBAKgMACZAgpAAZLyDAQCACrABMwEAeDwAU7SZAQCEDABTOKUBAIwwAFPEpgEAxEgAU4izAQCotABTMMEBALQMAFPkzgEAbKQBU1DSAQB4DAA1yNUBxAImSNkYADXA3AEMAzVE4AHcAjXQ4wGoA1NM5wEAiAwANdTqAdwCJmTuGAA17PEBPAM2gPUBoAIl+QHEAlO4+wEAVBgANQz/AawCU0QCAgDwDAA1NAYClAI1+AgChAA1dAwCfAJT1A8CABhwAlTsEwIAUEQBQx4CAHQsAVOwHwIA5EQBU5QrAgCADAA1FDcCsAFTlDgCAAA4AVSURQIAADAAQ1ACAIQkAFMYUgIAbHQBU4ReAgAgDABTpGoCAJAkAFM0bAIAoEgAU9R5AgAYkABT7IMCAPwMADXojgLcAlPwmQIANAwAUySlAgAsVABUULECADjgAUO9AgBcDABT5MkCAEBgAFMk1wIATAwAU3DkAgB4DABj6PECAFwOUAFDAAMAaAwAU6wOAwC0hABTYBkDAKx4AFMMJQMAuAwAU8QwAwDUDABTmDwDAMx4AFNkSQMA2AwAUzxWAwD4DABUNGMDAPCQAENxAwD8DABTIH8DABh4AFM4jQMA/AwAYzScAwAID0gAJasDeAY1rKsDBAJU5K4DANRABSWyAxACU6i2AwCM7AE1NLsDBAJTlL4DAPwMADWQwgMQAlSoxgMAtOwEQ8sDAPR4AGNQ2wMAJBFMAkPsAwCIDABi/P0DAKQSDABjoBAEAKQQRAFDIQQAxCQAUwgzBAAQJABjGEUEAEQTYAAlWATMADXMWAQAA1RgXAQApLQGJWAE6AVUsGMEALzwBkNnBACoDABUFGsEALhwBURuBADAYAZDcgQA0AwANVx2BDwGU/x5BACwDAAmrH08AFNkgQQAyAwAVCyFBAC0zAYliATQBVSkjAQAzIwHRJAEANzkAxaUtAAm8Jc8ACakm7QAJmCfPAAmLKN4ACbkpngAJqyqtABUfK4EAODAABaytAAmDLbkACbMuTwAVJS9BADYIAEWwbQANTDFBBwCJgTJtABU4MwEAOxIAEPQBABUjAFTINEEAEycA1Rs0gQAmCAHQ9UEAAwMAFQQ1wQANIAEQ9oEAJgwAFPc2wQAwCQAVJzeBAA0LAdE4AQAXPwAQ+QEAGQYAGOQ5gQALAbQBUPsBAA0DABT8PIEAEQMAFM0+QQAeDAAU6z7BACwMANTXAkFALgMAFMUFwUAyAwAU9wkBQBwMABTTCcFANhEB1MkMAUA4AwAUwQ5BQDwDABT9EEFAIQwAFR4RAUAzAgEQ1MFANQMAFMYYgUA5AwANfxwBQQIU3BzBQBESABTtHsFAEwMAFMAhAUAXAwAU1yMBQCIMABT5I4FAExIAFMwnQUAVAwAU4SrBQBkDABT6LkFAIAwAFNovAUAeEQHU+DFBQCADABTYM8FAJAMAFPw2AUAlDAAU4TbBQBstANT8OoFAHQMAFNk+gUAhAwAbrp1CgwEAAEAAK8ApiRHbG9iYWxzIAIYAACXAPcCVW5pdHlQZXJDYW1lcmEAAJA4ABcMIABbRHJhd7BYAABwAAB7ABcLFAAEXwAgMgMMAC8DAAEAD/AFRFhCQ4YDgzKq3IpBXmm+o/zpwXV1AjEMAwBAABAsFgEAagJwAAAASVNHThAAEwHWABcgSAAEbADzBg8PAABQT1NJVElPTgCrq6tPU0dOaCAAADQAE1AoABgBNAAAzgELTAAXARgAFwEYAADiAHUHCAAAU1ZfZwCAVEVYQ09PUkRwAEBTSERSHgPiQAABAI0AAABZAAAERo6qABcTEAAAVAAXBhAAAFgAAL4BYl8AAAPyEG8FdWcAAATyIBCwABBlHAAlIBAMADFyIBA4AEBoAAACCABiOAAACPIAMAAiVhUIAAR8AAAOAGoyAAAK8gAYAADkABMGcAAiRg4IAA8oAAEA9QUvphooAA8AEQYq9h8oAGo2AAAF8iAUAACsABMiPAATGggAIgqAGAESBSAAMgrSAOwAIgYHIAAhAkAHADU/AAAIABA/XAATwhQBNKYOEMMBJAcyFAASCggAE8ZMAAB4ABxyJAESgoABEwHUACpyABgAGAAkARYC/AAMKAAYAiQBCCgAA6gBBCgAGAMkAQMoABE+fwAXAW4CD2QDCQQmAV9QT0lOVHAD////VxQMcAN/X0NPT0tJRXQD////UBsC5AYPgAP///9gBPQGv1NIQURPV1NfQ1VC9Ab///9eD4AD////ZwxoCg+EA////2AeA/gND5AD////bQQmAV5OT0lTRWwYDwQO////SQRwAw7oGw98A////1cPdBj///9jHwP8BgUPgB////9rCAwHDggVDwwH////Yw+MA////3MPlBj///9wHgQoDg8wHP///30AWz2vSEVJR0hUX0ZPR7gY////Vgx0Aw44HA+AA////14PPBz///9kDgQHD0Ac////bg4UBw64GA8UB////2oPkAP///92D0wc////dA44Dg9QHP///3oO3BgOkDgPRA7///9eD6A4BA+MA////3MO5BgPPA7///9gD8A4////eg8sBw0OaBUPLAf///93D5wD////gw8EGf///3AeBWgODwA5////dR8BOgMEABwAHwOMcRAAZgIA73GXX0xpZ2h0UG9zAgMIzHETQBQEAigAX0NvbG9yKAACE1DYcRJf0XF+Rm9yd2FyZFYDAAEAIrABegKxX1ZvbHVtZXRyaWNeAA9YAAEiwAH6AV9fTWllR6QABCLQAXwEsV9TYW1wbGVDb3Vud3gMOAEvFAKAcgsAHwAQFEgAoldvcmxkU3BhY2UfAAMfAQzIABRA9ADfWkJ1ZmZlclBhcmFtc6QAAwSYdhMF8gMDKAHJRGVwdGhUZXh0dXJlsAEUBFwAaURpdGhlciMAFwE8CRUPnAEDIwApQjAQOxsE/AEvAQCIcwsINAEPTAIVHwdMAuQBfAEhTXlwAHVNYXRyaXgwvAEXBDAALzABeAJvHwZ4AjQfA3gCGBwOJAAaMDIHHwicAiwfBegE/4kUDXwAAfIAM2luZ/kBADAAFhaQAZ9yb2plY3Rpb25UAAkcIBh4g1NoYWRvd3OgHAcVEFAAAhkAT0RhdGFIAAEEaH4fCAwDYBERuAEBnwA6TWFwbgAPEAMxD3ABAQQoBg88AQEI5AEOTAMP6AX/qA94A4kfCXgDNB8EhAY8D5wDCRcEJg4PnANlD4QJER8InAO8AHQQZF9Ob2lzZVYCDigAI+ABpAABKACPVmVsb2NpdHksAAMf8DwHNASQTV9fVGltZcAABB8A0AnEFQ1sAQ/ABgMfBmwMUB8J6AL/OQ/YBhcPFAOTHwdgBoQPOAMFBFwGHwYIDS0PIAb//wIPCAr9D7wGCQSEAw8ICgoPpA1xDyAH//8uDxQEiR8KFAQ0HwW8B1gfBDgEEB8FQA5lD8QX7AE4BCBIZUcAP0ZvZ3QDBB8COBr/bB8IdALiD2Aa/7wPOAXjD4ga//9tDzgG4w+wGv//vQ+oEP8SD4wHEw/YGv+0HwoQA/83DwAb/+EPcAb/Ww8oG///tQ9wB/84D1Ab///hLxEAAQADJjYBRAAP4DgP8QGeO5vvuPvUo6Pg530gUo4V5gEWAeA4F5zgOA+sOD4fAKw4Bx9PUDkRAkAAclRhcmdldADgOBc4RgMElDgD6Dc5CPIgrDcJDAEZPikEBGABFy/GAB8HeKQAj44IAAACAwIDbAET8AG3OvrWKKV3HirYuqm51DvTTgAvaAhsASQfA2wBBB8LbAEEHwdsAT8ikAdsASrkAUw6GyJMOgBUABBaSwASYIABBAwAFwEMAACkAGJYGAAEAHAkACZVVRAAGwEQABMCEAB1ZCAABDIQEBwBdWIQAAOyEBAMACJyECwACAgCAJA6EgcgAiQHMiQ5AzQAIvYfCABFRQAACTw6BAgAE37AOSRgEBsBIwniHAAiBhloAFcGiSCAQYoEUxAAAAcS3DkTlig6BAgAREsAAAUcABMKCAAAlAAEVAATVjA6NAYAEPw5IwsSHAA1KoAgkgITChQAFzoUAGgOAAAKEgDsAiiAPwQABDQAExDoOhMBADoAAQAXG6QAAJAABEgAFwoQOwAwAB8zHAAABMgAVEEAAAUyWAAVEFQ7FQoUAAMIACQCQHsDJAA+VQFUHQAACMJcOxUECAAFWAFHGgAABkgAI4CBGAAXN2AAKuYKaAAmRgBAAB8AiAAGIoA9BAACiAAEBAIJSAASfggABLACRCsAAAbEASIagEABHiE4AQkcAVQ4AAAHcuAAGgAUAgS4OxMBfDsBlAIWD6ABB2YHMwlyALg7FgLcAAgkAAB8AheCTAAXAggAAHwCBBwAFzp8PAwUAATUABMcuAAMIAATAfQ8ADwAExKQABcKMD0EPAAACD0T4iAAIgYJPAATNuw8HwOQBQQTNqQAFwNwABM2NAMWBHwAYwAwAAABIcgCEwSAABsEkAFTAwAEAxoYAAS4AxMEZAMIuAMbALgDEwWcARcECAAEHD4TBWwAEwV8AwQ8AAQ4AgBCCCJWBSAAADgCEwI4AgTIPQTcAAhkAQXMAwMcAAB0AQhsAARgARMGHAAUQoAAAzAAABQBhDuquL8ZAAAFHAATKggACEwABBQAExoIABNEWAEI1AAAMAAIKAETBLACFwU4BBoEIAEFRAFEMgAADCAAJiqAXAETHXQACKABEh1mBxQFMAAEIAAEsAAIFAAC9AE/wD8ZMAAAGA4EAi2AIHAABLgBCDQAFzooABMyVAMXA7gBAAgAEwQ4AxMDJAAEyAIIoAUXAEwCEx48AgiMAiUBQI4LGAHkPwRYAAR8BQBWChM0lD8IgD8PIAMGBIBBH0DECAkXNB4IHwjMqQSPbgkAAAIEAgTICBPwAf5mNl0NRYt1RGmRkkOcS/lSAC9ICcgIpyJwCMgILxwCyAgzBAwAHwPUCCAiWDAQAB8D5AgsHwjkCP//Mh8D5Aj/HBQIHAYPsAUFI/IA8AUfDtAIQwBcCR8D0AhgEzj8AAQ0BwOEAAX+EgD0AQQgACYGiXBIFAZwBxIOHAAPKAABExUoRwgoABgAcAAaDjAAAPISQEoAAAu0ABMH6AAEtAAXA7wLA3ABHMFkCRMG/AEfB6gJLARcCQ+oCf9CBYQJC6gJBIQJCMwJBHgEGwZ0CQ+oCRkARAAfAqgJXg4AsA+0Cf///////////4gMAQAPNLYBD5Ad/zobPJAdDzC0DSDCCiQLHwQkCxTwAWyHBnaRlFUSx6XGM2eXzgpmAC+cCiQLpyLECXwBH3EkCxAI7FcXAxAAAAwAHxlECwkfCEQLex8HRAv///8/RDYgAAZECyI6gBAEExioABQiXAsDiAgEHAATAZgCAHgAH0IYHgwE9AcqRgIYHh8EGB4MGwIYHgT0CQyUCxSCkAgDyAkMlAsAGAAIeAEaBZgbDZQLJoIAFBwXBQgABGQBBDQKHwSUCwgv9g+UCwNWNAAACYJACiWAgfpeBgwAFzRoABcqGAAlOgBoCwVEAAQUAAQoCgROeBM0yAAIJAAAOAFMrMUnN8gABFwMBERcFw4gABcaGAAPgAAJGwqAABoAZAABHAAXBTgCakcAAAuCAGABKgZwDAwEXAAE8AsIcAAWGpgCCLACARAFHwZ0AAQXMrgACBAMFwZYAB84OAEABIAMBIgAHwJ4DAgbAkQAACwMGwJgABcSdAAbAngMBBwACAQgD5AABQB4DAj8AhcEoAQTBYQCCMgCADAGDNwCDHgMH4J4DAQfOngMBA+AAAEMFAAIeAwMMAAEbAIMeAwHUAAJ4AMP1B8FFwR0Awj4AB8EeAwYCIwiHwPUHwwfBHgMTh5BCAsPuLsOj6ILAAACBQQFDAsT8AGrHmx9pymZ2/13QYKKJDlhagAvfAsMC6cipAoMCx+pDAtJCFAWFggMCwQYAB8EGAswBBAAHwQoC///ch8EKAv/XQS4Bh8BCAsHLwjyVCkCDxQLMx8EFAv/gRcEtA4PFAt9BBgJBIgYCBgJFxTsAxcGGAAEZBgEUAIfBigABARkGAgoABcAcAAqRgIwAAhkGAkgAA9kGAgN9AEP7Av/kR8D7AsEGwMQDA/sCwEfBOwLAQ/AK24M7AsOIMQP+Av//////////////84PXCQBCPR6D+hB/zoXNfxFHwmEygwfnngvGvABE+nlEWQg/RfqpJ90qjUh7mIAH3h4L6gToHgvHyh4L3EfKDQkMx8JeC////9CJAlS8EESgTQAEB/EAiWFIKQEAGAAEzJoBwWMBwv8DCkiAJwJDxANBRQI8AgPMCQZH0IwJAgTA7gJDyQNBRcDdAEfBiQNEATgJAiACh44kA0PJA0CGAfQCA9IQgAT4iQAIwYJSC4DRAATHhQAArwLJAAFKAAASAQXB2AAEwhwAh8HFAsAIgAgCAwEcC0bCBgKAGQALjggoAkXGoQACMAKFwpgCQR8IQVQCwscAAxMCxsEAAseBxwLDUwLDDgLD5wKAQQcAARMCxsDJDAXcqgOCMgBBEwLCNwBD0wLAR8DTAsEHwNMCwQMgAAPlAoBCEwLCDAAFw6UAAxMCxcDdAIINAAPTAsBCEQLCPgAHwVMCxgbQkgDD0wLCR8FTAtOHjrgCQ/o0AoQkjguLwUCNBcU8AEWCjPDvsxdWp2uqjkyry+oZgAfbDguqBOUOC4fZeQJNg8UF0QfKBQXMx8JFBf///8/DwAKSA8cLg4IuBYfAQAKWB8FAApgBMQ5FwdYNw/EOQEfB8Q5ABcFDAoPKAABBGgVCCgABMQ5CEgKDsQ5DlQKD2gVCRcE0AkTOjwABPgCGAf0FgfYCiZGAtgKCCgACNgKCBQAKkZ+MBcE2AoITAoP2AoFCUwXD9gKAASsFQuIFw/YCgIIqAoPjAoFHwbYCv9JHwTYCgQbBPwKD9gKAR4F2AoPCC5wDNgKD0DYBg/kCv////////////+yDAEADpjfD3BF/0gXQmAMHwiU3Rwf0nguGvAB4tieLbJq7ZDhe0nTqX+KanIAH6x4LqgT1HguH7V8RWAPgAwqHyh4LkMfCHgu////fgG8DB8DvAwMFMLgDAR0Ax6EvAwfA6AhNB8D0EUAHwS8DAQIWC4PvAwBHwWgIRwfBbwMCAj0LA+8DAkfBsQuCBcGxC4DTAsFDAAXNGgAFyoYAAQoCwRELggUAA/ELgUIJAAMxC4IHAAPxC4BHgbELg+AAAMPxC4ICFAuHwbELgAIYAEIxC4fAmQtAAhwAA/ELgkfB3QABASgDBsGxC4fB4QMAAncAAdIUgigDR8EoA0AHwOgDRwbA6ANExIkAA+gDQkMJAAMoA0EBAEEaC4HwAAYCVgDCBwADKQMDGwBFweoLh8HBFMEDJgAD0wBCR9E0A0EHHIEMgugDR8FoA0MHwWgDQQfBaANBB8FwDAMDKANCIAOCOwxDKAND/QyAR8FXCMMDPgAHwWoLhwMYC8PXCNnHkckDA8s5hqPsgwAAAIGBAYoDBPwAQx+pVoyJj6bNqeHZIltqvR2AC+MDCgMpyK0CygMH+2gOmwEDAAfBbQYQAQgAB8FRAz//3IfBUQM/5UP4BgBHgP0Og8oGQAPMAwzHwUwDP+BGwX0Og8wDHkEeAkIqBoP9DoBHwf0OgAEUAIfBygABAioGgQoAAT0OglkDA70Og6EDA+oGggJ9AEHUF8OqBoPCA0rFwTQEQ8IDf/mHgQIDQ+oGhcPCA0BD6gabgwIDQ+k7wYPFA3////////////////kDsRIDhz5D7An/zoeNBhiDxj3Ci8aCcCKGfABg24VtG7Bzd+Uqc9vRirNPGYAH/TAiqgTHMxILwcCwIr////yGzKUDQuoMw+sig4PqFUBBNxVDNgmHwKUMwgEkAkPlDMCD5xVHB4DnFUPlDMWHwaUMwgErAwI8HUEvFESIGQAFwlkAAGcAAhoDh8gKFIIXzuquD8ZWFIICJRUCEhIF0KgDAgcAB8qSEgMHgawUw+MMkYfBVBHAAyMMh8FjDK4HgOMMg+UUyIbAiQ0D5RTZx45pIEP+PwOLw4KMFIZ8AEQsUUK+32qH9t8dJpUB7EcagAf6DBSqBMQaD0fRKiB/////xAPgAn/GgQUPQhgXQ8UPQEfBhQ9AAR0fx8GKAAECGBdBCgAGABwAAMUAA+8gQkfBhQ9GAhYFQgMXw9YCgkMLF8OWAoOvBMIWAofBZBdBAhYCh8FWAoIHwVYCggfBFgK//80HwOU9QoPZAr/////////////LQwBAA68FA9MR/9MDsh1D+QLDwiMAS9OC8yMGfABw/ZguuLcDmslYG+Z3NgPFnYAHyhQR6gTUFBHH5TMjP////9sDrgtD0AMLw/8RhkfErgtDARglg9ADBkfBUAMBB8SuC0QEwYMAQ+4LQUEZHQPwHUJGyK4LQSYCwQ4LQdEAA+4LQYMJAAIuC0MHAAMuC0eIrgtD4AACg+4LQEI6AAPuC0BBCAAD8yMBRcauC0McAAPuC0FH0J0AAgXMrgABHAMGypYAAiUDBsadHQL1AAPJA0CD/QAAQwkDQ+YAQEEJA0fIpAAAATUAC0qABgBDbAsCJB2GwPQAB8qRHUAB/B2DbAsHyIcARgPsCwJHwXURQAXELgDDyQNBR8SsCwEHwqwLAQPUJoBDBQACLAsDDAAFw6UAAiwLCoKAAAECDQADyQNBQQABAi0Lx8DzEYlDyQNbh9GiBcQD7w5DS9CDPhSGfABR+JADZERodafaMvPejYqzXoAHxy8OagTRLw5H9FwgZ4P+FL////VD8QL//88CHBfD8QLeg/AGbAMCIEIwBkPnAz///UfBCQkCg+oDP///////////////3MPaCYPD8iO/0gOEHoPfAELCDAOHy5EMhrxAU2x0JSsaEIqcg2aJ7jA5m74AA8QeqgTMEQyH0zYjv///////xoPSDJlD7wxAg9oj///eB8/gAocDKQYLyILlIQZ8AFhJkxvijzwAV2cK2Oel46GdgAf/IQKqBMk5DAfiZSE/////////w8IKF8EZMkPeAsBDEhfDFgXD+xfAQh4Cx8GrIQIHgUAXw8khf//bgx4Cx8E+BUVD4QL//////////////9DDAEADvwWD7iQ/1gOjHcPEA0bCJgBH2IoMRrwAXSlHwA7Lk4baD2rngyvnraCAB88KDGoE2QoMR/ZyJD//////////1AIBAEPLDEJFwcsMR8HLDEEFwcgAQwsMR8HxHYEHhqwdg9Ykf//ch9MxAwcD/A9Dh8NVIQZ8AGVgbeTEajY+aQ5sfGz2lRJhgAvHA3wPagSDMgMLxEDVIT///////////8wDKgNHwWoDWkP5IT//3gMqA0fBXwnFQ+0Df////////////////9xAGoMGwugTARiCGBTUE9UMgMUAC8DAHQNE/ABhgODMqrcikFeab6j/OnBdUoAJgwDdA0XYHQNDwQNDjUPAABBDQM4DQ+8KT4fCKgNBwB0DSA0AnQNSgEAjQB0DRsTdA0bBnQNAD4BEF+UDAMcC0RnAAAEoAwXAbAMBAwAANABEwK8DAOELSQI8igMExXsASSOINQLAWwFKvIAGAAA5AATBnAAE0ZcDA8oAAEApg8vphooAA8TEjANCCgAAFQCKvIgFAAE7FMEPAwEuOMEot0AGAwU0hALGQewAgCpNwQIABA/XAATwhQBJaYOhA0kBzIUAAP0CxfG0AocciQBJYIgwgIEkAYIGAAYACQBAzwDDygAARgCJAEIKAADqAEEKAAYAyQBJwIQZAMBPgsfAGwDExcN6BBQREVQVEhuAA94A////08fAngDAQ/4Bv///1sMuMQPeAP///9oD3wK////ZB4DhAMPiAr///9tDKAiD3wD////bA+QCv///2gOiAMPlAr///9qDyxJCg+IA////3sPrAr///9kDvhBD0wV////Zg9GAwEAygKiJEdsb2JhbHMgAjotAIBfl19MaWdodFBvc+YCFwQ4ABNA6AMCKABfQ29sb3IoAAITUBJT7l9DYW1lcmFGb3J3YXJkOgMAAQAisAFeArFfVm9sdW1ldHJpY14AD1gAASLAAd4BX19NaWVHpAAEItABoDjuX1NhbXBsZUNvdW50AQAcASIUAr4CMV9NeXAAdU1hdHJpeDDeMRcEMAA0MAEAhjWSAFVuaXR5UGVy2wAzAACQugEAEjCyX1dvcmxkU3BhY2UfAANLAQz0AAQyVO9fWkJ1ZmZlclBhcmFtc9AAAxRwQgIDAgQDVAHJRGVwdGhUZXh0dXJl3AEUBFwAaURpdGhlciMABHwBBSQAAfoAAyMAGjDwEwAkABwPJAApQjDiBBsETAIIYAEPLAEBCIQBAJwCHwScAhAfCJwC/xEDLAABfgJ/MlNoYWRvdywAAh9wyAJwFAx8AAKoACNzoLgEFRC8AQIZAE9EYXRhSAABJoABKAIPEAMxHwQQAzwREeABAcMAOk1hcJIAFwSAAw84AyUMQAEWAYI1D/AFFh8JVAO8AGgKZF9Ob2lzZTYCDigAI+ABpAABKACPVmVsb2NpdHksAAMf8EQGYARsIF9fVGltZewABB8AoANQHwdYA4QVDbwBD1QDAx8GjAZQHwo4A/9lD+AGFg9kAzEPBAeYHwmsAzQfBawDWAHKVQ8oBwwfBSgHUQ98CuAB8AMgSGVHAD9Gb2cAAwQfAkAN/7wfCcQC4g9oDf//dQ8wCv8SD5QGEw+QDf//BR8LYAP/Nw+4Df//mQQwkAiUtggIRR8eEJAa8QCWNKnBdo8sRC8e2b2MR8Yc1h/4VMKoEyAQkB9IEJBxHxhQtv///4YE5JcYAvAAC1TCE+IgAA+wSgUU8uB1Fg7QUR8SNGwEFyI0eA+wUQEEPGkEpAMSBCyPBaRSBLB4FwSAIRMFlE8BhAAeg5RPFwUYAASUTwjwvw8oAAEElE8IKAAElE8JBJ4Wg5RPBJgEEwV4AhcF2E8EsE8XBhwACCBSCLBPFzHImRkqJE0BLAIP5JkEL4A/AJoBJToA3E0nByJAaAgIAARAIwgM1w/AngUE4JwIbFAXARRPBNwBCJzWBMRRGwOYAgUABQcYTwhEABcCkAAEGJwfBBhPBBdCLNcbBEwAGyrcAAzwnx8DDFQEBFQADPCfH0RwAAQXcvQBGwT8mwxMAAz8mwVQAAtYTwW8AAtYTw+AAAEMFAAIWE8MIAEXDpQACFhPB1AAHAg0AAxYTwQYBAQUAgionhsCIJwPWE8BEwNYTxsSFAQPWE8FAEQAHwIEeV4P7EsCD/Sb/z0eQpTYD+hJChGe4IUfA9B4FPABB7zWWvq8x7h1EI+Q659CXmYAH3hIwqgToOCFH6jQeBkP6KlhC/QLL1gYwHj///+GAJAAGxIQXh8C0KkAHiIoDA98zpEFvOEuRoI8DAnUCg88DAAfBjwMABcEIFkPKAABGxUECgS0pwRwAAQUAA88DAEEIAwTBkQAHAbsWw5Aqg08DAgEhxsGPAwPKIMDL4A/RIMBBCRcBIwCHwSozxAPPAwFCCiaDjwMDsCDCISmBEAABagBAyAwFxg4MBcFGAAYF6gBFg7kDQwoABgZqAEWDuQNBHAAKkYOMAATGqgBDAQOBFBaBNheFwTkDQy8qgTQDU8yIAAKEM8BBNheBIADD5gBBQO4WhUKeM8PWA0ADHzPGwOgzxsD7IUOfM8OxOQPkAAJD0itGQ9YDQUPcAABDGicDLzPHwS8zwwfBLBcBA5YDQ7wAA/k5QUIsFwI5OUEsFwfBLBcAB8EMAMMD7zPARsD+AAPvM+nG0DgkQ9MUgUfQryRGvABEElIHn5C/13yLBolGHXTOl4AHxzcC6gTRNwLH5G8kX0fGLyR/////4oFmKYO4AsLsGcfg7BnBB+DsGcUH4OwZxwP4AsABMQLEwdEABsHeJEEHAAP4AsVHwfgCyweCJQJDuiSD+ALEB8G4AsgDUANDyySEAicAAh8AAVUDQ8skgQEYAsfBiipHQ+YqDgMfAEObJEPgAszD+RsEQ7sAQ+ACwsJ4AELMGgeBDBoD4ALrh4EgAsP3JG8DlRaD+QY/0wbSAAND1BYGRCuwJEvBgNEdxTwASmLWRgjFvsgpA4CTZX1hh9yAB+IwJGoE7DAkR/s8BgZDzR3bQ8wDQwEIAAfBUANLQ80d////z8PDBkCDxx3JBxiHHcfgRx3AA9kDUQfgmQNCAhEIw9kDQEfBGQN9B8EZA0IDpAKL4A/OJ0FHwhceBQfBUDEEAW8cg5kDQ9g5wkYCMAlHwVEGQQfBkQZAAQIGR8GKAAEBEQZCCgAFwBwAAQUAA9EGQEIBAEEnHMERBkERCcPHHgBBNgaBEQZCDgBDkQZD9CeCA+ADhQfA4AOJB8ErKAAHwasoCAMiMIfAhygBATAGQ8coBEfChygQA9keyoPsHb/BR8EsHYsDAwSD+Craw94/g0E8AwfvrgxGvABDpP1kh3HJBPOpu5MK8mmV2IAH5jAq6gTwLgxH3C4MXIPDPT/////Qg+QJf+rBNyDHwQ0wgwXBIiDHgQ4qw/UFwIIOKsM9BgbEuALCIAZD3z0AQjQJA9gJgYPACX//1wOgBkOSGkPhBn/ThtHhAwPRGcdLxYMeDIZ8AEUHYd/YN8+gRqMiNORTwVKdgAf8JwmqBMYiBkfxngy/////2APSBkNDijcDxQZAi8I8hANAg8gGSAP1Az0HgTUDA8gGSIXB/QEHgXUDA+I2AoM1AwvBgAgGRQPZDKoBSgBD2QyEAw4AQ8gGRUI8A0fEiTQDAdkAA4k0A+Y2ggE8A0PyNoBFyJ0AAHskQ/wGAgfCvAYEB4aJNsO8BgOjAAPHAECDzjgMg9E3P8BBOgND0TcKQ7wGA/wDWUfRUjQHARkDB/SADMa8AGwqWFTu/G5SKmeIVHaDPEnbgAfrGQMqBPUADMftQAz/////////38MKNwfBSjcaQ+QM///yA4gDA+gM/9cHk2A0g+0diYvPg2wMxnwAf12ghd/NU29JxOj2lpVVOmCAB8Y9KqoE0D0qi8QA7Az///////////rBDQbD1zUCQd09w9c1AYMmPcMNBsfBsgzCAjAAA9ANP//tR856GUACIi4j+IJAAACAwIDdA0T8AFg+WfZ2EH71rnxctzEd+jKTgAfvOxlqCLkCHQNHzk4GzQPQA0WD9Bl4BgALAsLoAQPCAsFBGwABAgKCJAAKkYCCAAEyB8bAXgEExyADQjoUwMwACsKEiALBAgAFwqADA/oAAEEDLYMLAAaOrgMHAg4AAggAA8gDkUfQiAOHB8qIA4QFzIMhgTYASNWBRQNGYnoDCYIEngOBewABFAABGgOHwJoDgABFAAKNGUPaA4CEwJoDhcCaA4XAmgOFwJoDhcCaA4TAmgOCGgABGgOFwJoDg6IAA9oDgMJSAAHaA4TAmgOHyJoDhQbGmgOHwJoDgMrCXIcDiL2DwgAJoYDlAIPJGecJAXi8AAWCXAoDoA1D2zTBQ84Z0MqVg4MBRsADAUEAAgODGYPbCcPCZwMDvz2D/BlGgkIDAdYChdC6AgP8GUBCCBnCDQmGyqsGh9E6CQECCgBCHgkBJwSCBgBDBgKHyJIGQQfGkgZBAxgZwigZh8aSBkADBwAGA4EAi2AIHAADsRnD1gzBytWBewoB1gzBdRlGg4YCgRMAg6wZQ9oJW0bPtQxHwGUwgAfwlA+GvABandpMbvqeHBfTzL7E9CVFlIAH5xQPqgTxFA+H3FQPnEfMFA+NA84Cv///4sfAzgKYQ9ccf8oLwADJApgBPzqCLxxD/zqAR8F/OoAAcQIB3QxDCgACLxxBCgAGABwAAMUAC8GiVDBAwDoAB8FUMEMCAABDFDBD/wKJQikMA/8Cv8+CKxwCPwKD6xwqx4+VEkOnM0PCAv/////////////2h8/XG4MCBQLH/IECxrwARbkwSTEn0YTmH/ZPI3ETR1iAB/MBAuoE/QECx99BAtxC+DyD1QtJg8EC/////+XD5g8RQ/o8xsPmDxXDxBFCh8DbDsQHwNk2QwfBHwgBBPiJAAuBgks8iYGCVA7BCgAKlYO0NYTCGwCD1TXAQxQOx8IUG5oCDwrDBDWD+z//z4fBez/BA+c8RIPgDoBD5zxbh9ENAsMDDwWH+aYRhrwAdmawrK0b63RaFELWscS3FtmAB/AmEaoE+iYRh+6mEZ9D5zjBQ9UC////8sfBFQLYQ/sR/90LwAFVAtgBKQWCDg6D6QWAR8HpBYAFwVgCw8oAAEIODoEKAAEpBYInAsOpBYOqAsP/OIJGwSI4R8I/OIUHwL84iQfA0w4AB8H/GskCLjkHgO8Nw0sDA+8Nw0EFJQPLAz/Rh4ELAwPrEasHkS8eA8Q8AMPOAz///////////////8IG0a0Qw+gLgEILPwBYGsvBAScIxT/AbWJVf68vQ4O2qPDzojbNbJga+UPLPwaHwjALnsfB7wj/////5QPSP0sLwhCPEQMBHArDMAjD0gMAg9EUlAIfGcPwHYZDJxMHwQYURQPJP0BFwUk/QRcagQMABc0aAAXKhgACEhODIQKDyT9BQyYTQgk/QxkTQwk/R4FJP0PgAAGDyT9BQw0Xwwk/QhgASoGcKwMCBRRDFhRDyT9CR8GdAAEBBgMDIwvHwbQTgwM6KsfAvQvCBsCRAAMdFAIrFAOoGsP+FAYD1B5/AgIUA5AXw/0LwwPEFCCH0tcDAAMfBgMYAwR9sB3HwSMGBTwAQLUjRxLScgVsBxClSsPHIRqAB/QSJGoE/hgDB/+YAwpD9B3YQysGB8wrBgzHwesGP////+UD3wMPw88eX4vAARoDP+BCJAYD2gMfQ9YewIfglh7BB+CWHsUH4JYexweglgaDFheD1gaCQn0AQ9ADf+RHgNADQ+geKwMQA0eA8glD0wN/////////////////yMXTCAzHwggMxAISA0vJg1IDRnxAZpwRB0Q/8wfYSHF8W49+kyNAA8seKgTKCx4LwoD9CZfD0gNKh8oSA1DHwhIDf/////TD4x5EhzCjHkfhIx5AA8ISzQfA0gnAB8ESCcECCgND5QNAR8FCEscD2B4CQjECw98MwkfBpQNCBcGlA0EcHYEDAAXNGgAFyoYAARECwQUDQSIAB8GlA0ICCQADJQNCBwAD5QNAR4GlA0PgAADD5QNCAggDR8GlA0ACGABCJQNHwI0DAAIcAAPlA0JHwd0AAQElA0bBpQNHwdEMwAJ3AAHPFcIYDQfBOR4AQ9gNBwbA2A0ExIkAA9gNAkMJAAMYDQEBAEIiJMXB2A0CBykBBwADGQzDGwBFwd4DR8H+FcEDJgAD0wBBg8UePwEcA0M+AAfBXgNHAwwDg/ETGcfUXgNCQ/EGhAgBg6ohR8EqIUU8AHVzSUDb91LCEKnkySYBSNVdgAf4HwNqCIIDXwNH0J8DSkPuIVtD8hADAQgAB8FmA3////LHwWYDf+RD/RAAR4DGBsP9DQAD4QNMx8FhA3/gRsFGBsPhA15DyyJAh+CLIkEH4IsiRQfgiyJHA4YGw7YDQ+8QggJ9AEHmGUOvEIPXA4rDECHD1wO/+IeBFwOD3CGIg5cDg9gNmUMXA4fBPROBQ9oDv//////////////////Oh4+yEQPeKICBGwOL24K6JQZ8AFxruT2ZieqqQwcIcNN33/aZgAfSMB0qBNwwHQfXOiU//////9JD1TKpA9sdBIO+DYO4FwP2K8NCHgNDwzJZQzwNQ8MySEMPHQMwDQfKthbSB8FRHMADNhbHwXYW7geA9hbD/A0Ih8CzMh2H0PM0xAMFBkvYgt4fxnwAUFqUHNmm4zcitxNMR/8qkJqAB883FuoE2QIaB+ZfIr//////2UP1Ar/GgS0ZwgQQA+0ZwEfBrRnAARIiB8GKAAECBBABCgAGABwAAMUAA+QigkfBrRnGAiQFgzEug+sCwUPxLo9CCRAC7RbD6wLAgh8Cw54qw+sC///Mx8DYBYKD7gL//////////////+CDnR/D5DECwjECx+CtAsa8AEmcis4KQBIli4GvZq50+GJcgAfXLQLqBOEtAsfoeSW/////////28OuAsPNNJoD3SX//94H0nUCxwMjBcvdgxMixnxAFTz1sAHm6XGEiFx+4y/wkg8H1B0ZagTeHRlH95Mi///////////ZAh4PA8A3wkMmDwM0M8PPD0BCMwMCBwADkxkD1A8Aw/ci///aR9JMN0cDmQkD9gM////////////////jw4Yfw8wMQ8I7FUfosSYGvABRI6efMkRcLRxyByR1Gpx7HYAH3zMDKgTpMwMH+nEmP//////wQ6QVQ+MMS8PfHEZHxKQVQwE1MUPjDEZHwWMMQQfEpBVEBMGDAEPkFUFBLR9DxB/CRsikFUELCUEEFUHRAAPkFUGDCQACJBVDBwADJBVHiKQVQ+AAAoPkFUBCOgAD5BVAQQgAA/EmAUXGpBVDHAAD5BVBR9CdAAIFzK4ABcK2OgIWAAI4DEbGsR9C9QAD7gmAg70AA4UDQ+YAQEEFA0fIpAAAATUAC0qABgBDYhUCOB/GwPQAB8qlH4AB0CADYhUHyIcARgPiFQJHwVUcAAXELgDD3AyBR8SiFQEHwqIVAQPxMkBDBQACIhUDDAAFw6UAAiIVCoKAAAECDQAD3AyBQQABAiMVx8DTHElD3Aybh9QKD4QD+hiDR+WzH4a8AEoLHXgWhIGYcdFA/te+zEZegAfcOhiqBOYzH4vJgMUjJ0PzH7//////ysPGA3//zwI4BkPGA16D2BAsAysiwhgQA/wDf//9R8EGEwKD/wN/////////////////8gO3H4PvDUbCPgNH7b4DRrwAQZh6FsmSLHa+B+jQCZBXkqCAB+Q+A2oE7j4DR8uwJr///////////+lCAQBD/wNCRcHMAYfB/wNBBcHIAEM/A0fBxR+BB4aAH4PUJv//3IfVhgOHA8UHA4P+Iwa8QElHrAkEXjDsyVkt18bn3UIFBwfDhQcqAP4jB9m+Iz/////////////hgz8Dh8F/A5pD4iN//94DPwOHwXQUhUPCA///////////////////8YPtVQBAJIMpiRHbG9iYWxzUAIYABAO6gz3AW5pdHlQZXJDYW1lcmEAAJA4ABcMIABbRHJhd7BYAABwAB8DcAAQEwtgYpdfTGlnaHRQb3PmDhcEOAAA1QAVCygAX0NvbG9yKAACE1C8ABJftQB+Rm9yd2FyZDoPAAEAIbABvgLBAF9Wb2x1bWV0cmljXgAPWAABIsABkgFfX01pZUekAAQi0AEYAdNfU2FtcGxlQ291bnQBKAAHiwEzABQC9ACHQ29zQW5nbGVQAAgoABYYKABfbmVBeGkcAQUZICgAP3BleCgABCIwAuIEf19QbGFuZUR0AAIiQALmDzFfTXkMAXVNYXRyaXgwsggXBDAALzABLAILEwLmBrJfV29ybGRTcGFjZR8AA+cBDJABFEC8Ad9aQnVmZmVyUGFyYW1zzAADE3AIABMGMgcD8AHJRGVwdGhUZXh0dXJleAIUBFwAaURpdGhlciMABBgCBSQAAfoAAyMAAvsABaYIACQAHA8kAClCMH4RGwToAi8BAFgDCwiEAQ84AxUfDTgDvACME69fTm9pc2VEYXRhKAADI+ABpAABKACPVmVsb2NpdHksAAMf8IwD/AQ8v19fVGltZZQABB8AsANQHwewAzQfBLADPBUNWAIKjgATBCgAHwbUAywPfAcRHwwMB/+tAywAAT4DfzJTaGFkb3csAAIfcDgHcAXACQKoAERzoAEAqhUDWAICGQAEjgUMSAAUgCsVD9ADhBERfAIBwwA/TWFw1AMAD6gHLAxAARYBxhAP8AMWHw7EB///Ag9EBBYP8AcxD2gEmB8JaAQ0HwU4CFgBGjEPjAQMHwWMBFAfA3wI4AGMBCBIZUcAP0ZvZwADBB8CsA///1kP7Af/Dw+0AxYP2A///6EfDVwH4g8AEP///xIfDxQI/zcPKBD///82AIAAG2IgdQTAAG9TUE9Uwg94mhnwAIQhy0zDijm8+MWjnmwE/dEBPwCcD+gupyLEDtoCG7HoLh8leJphHxjcdOcAUCQb4tQsBBQjRG8SgzoEAASMAAS8UBcCXCMXIoAAExAoJg8gAAEE5QgEIAAIRCgAAQAXJFhUFwJIJhcCMCgTMRRSCEAAA2QnJwA3VCsIMKBEAFDDRywABGABBPAAFwFgAQABABMjYAEEZAAMYAEEoCUXAtQACOgABJwBFwMgAAc8AAG0JhsBHAATAUAnBLQuFypAKwgMAAhoLBcqCAAXAqQpFwEsAQeQoQVwLxc43AAXKjAAA6BRGAr0oxoCBFUIJC4JRAAbAtChGyKkABcaQC8EHAAbOmgACCgvCEQAGzpEAAgcABkabC0ogEBgAAyYAAhgAAREAgeoLQVMABdLMC0EFAAXMXQABDAABJgABEwAHgP8LQCwJhcIrAAFgAAlKgBsdRwFzAAiNgAwLhcDNAAEnAEEQIEEdDATRhAAKqYKnAEfItAxBAAIBBOiDAAqBgRMABgyxIEDCAATVpwCJfYPfAMUCiQADzgnASYGAigAHzLQWQg01gUQSgcVB3QAAzwAIwYBcCkHRAAahogwA/QxBfQDHA9EAA+cAAEBFABWQBxGAAAIAAD0AAQQASpWDRgBNAYIEDgxIwxiJAAAICwXARgBBWgAAFAABlQAD9AyQh+C0DIcBPQBCIwyHwqMMgAE8DIPsAMBCBwAFwocAB8CKDMMBOAyHwHgMgAEuAIfAeAyDASMARcB4DIXAeAyFwHgMhcB4DITAeAyCGgAGkaQNA6IAA/gMggJSAAPLHkAHxLgMhQbCuAyHwHgMhAEfAMBEAAWEsAGD3yqfQQQCA/s9wQvCOI0BAgAbCsE5PcmRg6MMh8SmE8EFyJEhA9sMgEEoEwE6AcSBOSdBUgFBBxbFwRQMBMFCAUBhAAeg1AwFwUYAARQMAgYqA8oAAEEUDAIKAAEUDAJOF4Wg1AwBDwKEwV0AhcFlDAJ7PUeAFz4CGwwFzH8WRkq4C0CdAUPGFoDL4A/NFoBBNAuBLwIBKRLCAgABCxeCNzBD/ReBQU49wcoMQjYBwTcAQhswQSAMh8DoPYIHwNEAAAXApAABPgIHwTULwQXQvzBGwRMABsq3AAMJGAfA8g0BARUAAwkYB9EcAAEBFQALqYK4PYfQuD2DB9C4PYEHyrg9gQPgAABDBQACOD2DCABFw6UAAjg9gdQABwINAAM4PYEGAQEFAII3F4bAlRcDxQwARMDFDAbEhQEDxQwBQBEAB8CHD9OG2jskRsCDNsEABAv5hDIkRnwAdKezucu2UruArdrgU5sx6ReAC/AEAAQpyLoDwAQHvoAEA/IkWsfGMiRRA8cEP/////////fHwQcEGEPGJb/dAWcrQSYQB8EuEAFH4O4QAQfg7hAFB+DuEAcD2gQABMHwAITB0QAGwfUlQQcAA9oEAkIKAEbB2gQD2STAy+AP4CTAQQMlgSgAh8ERJcQHwT8uxQMaBAM/JMIaBAMoEQPiJYJCJwACHwABBQAD4iWBQSsDh8GLLAdD5yvOAx8AQ7IlQ74lQ5g0w9MAAMfBOxFHA/sAQEfRHAABBxy4AELOEEfBDhBDB8EOEEEDiQRDvAAD4DUBQg4QQiA1AQ4QR8EOEEAHwTQAgwPOEEBGwP4AA84lqcfahTTABcNCFBXREVQVEgsESBCESwRHwMsERTwAZvYALScINKK7H8Q6EkzzAJmAC8cESwRpyJEECwRLxEELBEPCBRQDyB7Xgw8ER8YKG00DzwR///////////oAJAAGxIgVB8CWH8AHiJ8IQ/4y5EF4OEPKBEBCSggDygRAB8GKBEACPgNDygAARsVWB8EPH0ccrQPDygRAAR0IRMGRAAcBvxRDsh/DygRCh8GKBEsHgc8DwlgIA8oERUfBSgRIARgAARAAAWoAQOUDBMYgAEEIAAEGAAYF6gBFg44IwwoABgZqAEDKAAXAHAAKkYOMAATGqgBDFgjBGBQBOhUFwQ4IwxEgAQkI08yIAAKjMwBBEwLBIADD/gQCQjsCQ+sIgEfKogRAAgczRsDaBAfKogRNA/QghEOrCIPiBELCUACCzjNDqwiD4gRrx4DiBEP3H4iDzjNdx9wZMEACKwiD5QRCSBSEqhhHwOoYRTwAFV0mg/xboDUNeQyAv6mcAU3PwAsEpQRpyJUEZQRH1WUERkPmGFtD9wiDAQgAB8F7CL//////////yAfBbAR/xwP0GUSHGLQZR+B0GUADxAjRB+CECMICDwxDxAjAR8EECP0HwQQIwgOtA4vgD9AtwUfCBBnFB8FbJIQBXBhDugRD4DdCRgIuDMfBegRBB8G6BEABKwRHwYoAAQE6BEIKAAXAHAABBQAD+gRAQgEAQRQYgToEQQ8NQ/QZgEEfBME6BEIOAEO6BEP2LgIDywkFB8DLCQkHwS0ugAfBrS6IAy0kB8CJLoEGyocAB86nN8cDiwkDyS6Gw8YaioPZGX/BR8EZGUsDJAWDywkZx9nQPcQBJQSL2IQVEUZ8AHJKiFoiBWzuDRLcCHedeNXYgAfPFQ1qBNkVDUf2VRFuB8IVEX//////////+wO0O8P2CP//1EENHgfBIjkDBcEAAkeBOTKD5whAgjkygy8IhsShBEI0DQPQPABCBgjD6gkBg9II///XB9trOQcBLAQL3YRBEYZ8AEX6AY2LLKflO5Qdc6B1hFcbgAfUNg0qBN42DQvHgQERv////////////////8oDDD2HwUw9mkPlEb//7gfb9TPEA8QNQkfuqRGGvABonfupjcQ0y9Mq1uwSEL+LXYAH5TMEagTvMwRHy+kRv////////////8KD9A0DQ7cxQ+oEQIvCPJYBAIPqDQgD7gi9B4EuCIPqDQiFwf8CA4Uxw88wgsMuCIvBgCoNBQPkEaoBSgBD5BGEAw4AQ+oNBUI1CMERAAP1CMFB2QADiwSD0zECAQsEg98xAEXInQAAegLD3g0CB8KeDQQHhrYxA54NA6MAA8cAQIP7MkyD/jF/wEECBIP+MUpDng0D9QjZR91tPscDxwSCR/iLEca8AGnjufVgqvtdFKWrnG7OBmMggAfvCxHqBPkLEcfeSxH//////////////////+WBPQGHwZUJQgHHNkP4NgGDEDZDCgTHwZERwgIwAAPvEf//7EPtJENLyACAK0TFw0cAFdGcmFtZYiPCHAABACSBb4LA4yNIFoDDAAvAwB8ExPwAW7V+W9zF14lCMN6gn9be0dpACY0A3wTEKR4BxIBfBMfcHwTAAkMExEH0BINfBMiAwMajgTCmxMBrgAACAAOeROgU1ZfVmVydGV4SYUTH0/0ExEfAPQTAy8DDPQTBB8I9BMHAMATAMKRUEAAAQCGoBMWCMATGw/AExsEwBMAYAgiXwD8EhMADAATMuASZmcAAATyIDATABgTNTIgEAwAAIgCEwIkExcCYAcBkBITFaQCA3gAFwE4BwgYAAFgAQN8AAAkBx8AKAAEAGAAKqYaKAAEYAcIFAAEMAAXA5AAEwHwBwQgAABMABcSaAAXARgAGBG0AwNIEg8oAAETE/AHDSgAGiBoABMUKAQbADyFBEgBJkYQqA8FkLoZEIwLI0AKEAATHKAGCIAMASwLFSCYEwF3CBcLIAAC9AMXEWwDBPIFHwWUAwgTAUYAUERJUkVDcAIvQUykA////4UaEqQDA67DD1AH////gh8CUAcAD7wD////mB8BOCICD2QH////lg+4A////5gPDAsDD3gH////lh4D3A4P0AP///+tGwFgeA/oDv///5QOsAMP4A7///+WDrgDD9gO////pg+AB////5AOBEAPVBb///+iD8QD////pA8wCwMPkAf///+iHgQMDw/cA////7keATRPD/Qd////lQ60Aw/4Hf///5oOvAMP/B3///+qD4gH////lA4kcA8EHv///6YOyAMPCB7///+uDtADDwwe////vg/gA////6QOMA8PqDT///+hD8ADBw8gD////68OyAMPGA////+2D6AH////kA5UfA/ANP///7IP1AP///+0D2ALAw+wB////7IeBUwPD+wD////tR8DcHgYFw5UeJtDYW1lcmEAAJA8AA9UABUTBWQEl19MaWdodERpciYDGgSikiUACygAX0NvbG9yKAACE1AQecRfU2FtcGxlQ291bnR6AhcBKAATFHwAX19NaWVHdAAEItABqATXX01heFJheUxlbmd0aCwACFAALxACHAELEwEGArJfV29ybGRTcGFjZR8APFBvcxYEAe8BHgAQeg9MAf8uBSh7klNoYWRvd3OgAaQAAIJ/EHUYABJfGQDMU3BsaXRTcGhlcmVzzAAECAAPNAADb3FSYWRpaQABARJAfn0DdAECNABPRGF0YSwAASKAAVoEAmAAAeUAIlRvMwAFhAAXBDgAF4AmBgCYAIJfQ2FzY2FkZTMAqE1hcFRleHR1cmVIIgPOoocAX0RpdGhlciIABpABHwQ0AQAEhgUP5AMZHweYAowAqAhkX05vaXNlkgEOKAAj4AEwAQEoAI9WZWxvY2l0eSwAAx/w7AI4BHQJX19UaW1lxAAEHwAQAyQEOOoC1AAJAwIFgEIfBigCDARMAg6kBA8MAv+cDxwFzR8EHAUQEwMEAB8E9AIUDzwDDg9ABTQfBjQDkSBIZXUAP0ZvZ5gCBB8CAAhvHwH0Bg0OzAQPmAHQD5gJ/+gfCIwH4A8UAxMPwAnkDzQC/ysP6An//w0XEbAAFwAIAGE6AgAAAgFl9w/0FhDzAla5uqKI9bvRhgn+z/po6YgBGgMO3J4PfBYoHwN8FgQfANyePyI8ARYCKk8A3J4fHcTCDAyQniZiELAWDCSeDQieAygADAieCEiYHwpImAAfGkiYIBcxmJYAyBU17/9/HAAQAdiOGyBEmAyoyh8gSI4BD3ihCRcfcAQfAzCKBC/yBXQCGfABk9mUPNNUfNr5a7aQCqnp0TcAL8wFdAI9D1ChVyL0BHQCKj0BdAIfInQCKA+koAkfA4ACaA/coBEXOKygGwa0oA/4oDkTMySUCDAAFwrclwDcmR8i3JkIDBSbEgGgAAGAoBcAjJEbA9SSAAAaF0IwGR8B6PEABKCgD7CRBgfEAwQUAB8S/M0ECFTyD/zNARsaQKAfAOzuAAT4mRcAgJ8XAhQACHCUDNyhFzJwoQScGgD4GhcCgKETHlQBCKgACOySAAiUBFgBDAiUGxq0AAQIlAQwABsauAAIFAAICJQMMAAEiKIEAAMMcAAEsPMINAAICJQI6JsE6BsH7KEcCOibD7yTCQ8MAgUI1AAPjAEFCKAADwAGCRcAmAIEeKAXAKyZBByiDBgGGypkAACEowxIBgwkAA8sBhcPzB8FDzQG////////Cw9AjwAPRAb///////8UDAgVD8iRAg8cFf//QhtOWGcPmJAVIGINTwcvAwOIAhPwAfJqf5JEDNgrhZJ8FGJ17S5uAC88DYC2JQ+4CG8iZAyIAi4ZA7gID4C2JhYIgLYPXLYdDiy2DxAJEx8KEAn/FQBMsASAsSUGEQQHFAoUAC1WBqgGBVCwFx184xOWdLIqlgVMsAdIAAlMsAV0ABMBNAgWBigACEywD4gABwZQsARMsAiIAAhMsB8B+AkkBKQBHwHYtwMcCui2JfYP7AEILLAFwK8PPAoFFwBYtw8cwgkEFLYPYAoEGAU4vA+05AIPHMJPCRi+B34wCfiqDCQAFwEkAB8HJAAECJyxHwgkAAQfA1zBDAQguA9cqwEElLgICLAECAAMlKsXCAgAHzHgwAQWAvoIGAp8rh8FPAQEAGi1DAjBAygAFr8EAAUABxeABAAvAABEwQAABAITBnQKFOK4rhgJWAAK+AETESgMFwQkrw+oAAEbMVTAB3CwI0A45AwIXMIADAMTAkYiBDQDFwUYABcIXMIXVlzCDCgAFwpcwgQoABgAcAAaDjAAHwsQsgAExKsESiMJ6LEDGAAbDJTEHwcoAAQbDpTEDxCyBgMwABcPkAAfCJAAABcRaAAXCBgAGxCQAB8IKAAEGxKQABcIkAAIsAIEMAAXE5AAHwmQAAAXFWgAFwkYABsUkAAfCSgABBsWkAADWjgFcAAqRgIwABcXXA0EsAEaBoQBHQnsAQfYAgwkAAAUABcI9MUHXAIJJAAXCeyxCFgCD7znAQfIrw90swYEMLITBIwGBLTECNizBJjnKjcA4NYPVAMAAaAND+DDAQxUsgQoAB8C6A8IHwLo5wQOYOgPMMQQD9DCLB8B0MIYDygQCQwAEQ8oEHkFlAkDKBAAcAIEpDgNsAkfiSgQAwQ0AQ8oEBEfAigQqg6wDQ+QmgYPuA3/////////////////lw+0dwAPyA3/////////////////nw+oKwEIxFcPoCv//z8XLcoBHwdQrRAgkggYEC8CA4ACE/ABayyifZbC8YqbagZwHJIEH2YAL2wIGBCnIpQHgAIf5VA0HA4I4g8IEBQfKAgQMx8HCBD/+R8CCBCMD0jbIR8BSNsTDxwQAQGMCx8FEDYMCzwQD0jbLgxg2B8EYNgwFwLoEQ+0/TEIVNYI0NcfBNDXCB8E0NcIDwj9DQgw1gSI6gio+x8FSAv/AR8FSAv/Bw7YCA94sgYP4Aj//////////7oP8CEAD/AI///////////CDBgdDsi8D8xI//9RG1TgXQ+YuyGPcg4AAAIEAwSUAhPwAbcnbdRJuaIfHEUFh6wGpu96AC9MDnALpyJ0DZQCHl1YLQ9Y/z4PjAsdDED/D5wLJR8LnAv//5oOdC0P2PgnHMLACx+EwAsTBKgrD6T4BQSsLR8ErC0QCDwoDqwLDwD0DwysLR8HJAAEHwGILQAfBawtCB4JJAANrC0fBpAtBAA4AR8GkC0EF0IcAAiQLQRY+AQcAAhsKh8xiPcEDKwtHwcELQwErC0IzAwPrC0dGgYU9ScAB6wtEwcUCx8GrC0MBHQuHwesLQwdBhz1BqwtCAT5D6wtAR8GrC0ABIQrHwYoAAQErC0IKAAYAHAAAxQAD6wtARgIlPkPrC0AHwisLQAfBRwtDBsOPPsPHC0NHw8cLQAfBawtBB8JrC0AHwUcLQwbEpAADxwtDQisLR4KkAAJrC0fCqwtAARoAB8KKAAEGxaQABcKkAAXChQAD6wtAQSwARoHhAEdCewBB9gCDCQABIgtBJz8B1wCCSQAEwrwBRsHWAIHAAUFmP0lOgAgAA8g+AQogD9c+yaWBVz7FwNc+wQ0AC43ALj6C1QDBawtCID7DqwtD2T7Qww0EQzA+g80EQEMJAAMNBEEgAUEGBEEKAAENBEEmBAbAhQBHwOY+QQfGjD7AAjYEAhoLgh8+wx0+Qj8Lw90+Q0eAXT5DbgvD1ARwR8EUBFIHwNQEaoOzA4PoMYGD9QO//////////////////+0D6jRAA/kDv//////////////////vA4gTA6w3A/4Lv//PxcsPgsfBIDbFCD6B5ukLwIChAIT8AG3UOAqr22WI2xYdtnpAGihagAv1AcwEaci/AaEAh+/WDooDwgRBg/oECwfBBRc//82BDQAGypwgQAEBxsiMAAEaAkENAIfBNCBGBTCEAEPoBAEBViBA2wABHgQHwIEOggfEvA5BATsEB8K8DkEExoYABcALAAbOgAJEyD0OgQgAAQoCAhQABcgRAEDJAAImAgFbAkIHAATODgQEwN0ARcBHAAIwDkbOhwACMA5BHAADxA4BRsDVAATcigAANwMEwOkCRMDlAoXAngAHwAIAgAEGAEEgIIIWAEPgIKxD7gKAR8CwDn/Bw5ECA8Q4AYPTAj//////////yYP/EoAD1wI//////////8uDmhKDjDpD2xK//9TF1PkCh8LAOgoH+7IdxrwAdUS1XnNOlCR5Sbgk2QleKh+AB/IyHeoE/DIdx88yHesHwvId///wg54LA/IdwgPtHcQBGQLBjhyBjALHxJkLAwEzHEPyHf/ugU4AQ/IdwwbItBzCGQsDzh3BR8JEHcEHwg4dxQfCjh3HB8LOHcBD2QsGB8EZCwUHwRkLCgfBGQsGB8EZCwUHwRkLCgfBGQsGB8EZCwUCCgAD2QsHRsGZCwM7AEvBgAkAAMIDDAupgoQeAgkAARkLAgMBBcAAAUXCmQsFypkLB8SBAMDAWQsBUgDBHgDC8h3FwpkLBcSLCsPUAABAFwBDGwAD2QsAQS4EAQgAB8EuBAEAyAABdQAD7gQAQiQAAy4EAgcABc4NAUENCwEHAAPNCwBBBwACDQsFxJAAAjkAAiMAA+EKgUIjAAIVHgFtDAfBVR4HA80LAEPVHj/JQ80LKsOTA4PhPIGD1QO//////////////////80D2xJAA9kDv//////////////////PA+MLQ8PHJX//00eMmSDDzj3Gi8iCVyDGfABoxM/KI3L0XYuVUJcZrzAs3YAH/xcg6giJAiQAi8JAlyD/////9MIyFUP+AsFDuhVDvgLD+xVAQT4CwwcAB84JAsADBwAD+yD//+lDngJD9z8Bg+ACf///////////1sP3CMAD5AJ////////////YwwIHx4DCB8PDJf//2AfWWx5Dw+kAh4vAg9keRnwAfIgsNtDWnf8zNppl0r9tZiKAB/cZHmoIgQOpAIfgWR5/////////////54E7DAYBjgxD+wwABcGOA8fBuwwBBcGYHkM7DAfBnx5BBsanAAP9Hn//8kObA8PECUGD3QP////////////////////VR4FBCsPhA////////////////////9QUAAAAAAA", + "stageCounts": [ + 2 + ], + "m_Dependencies": [], + "m_NonModifiableTextures": [], + "m_ShaderIsBaked": true, + "m_AssetGUID": { + "data[0]": 2280896985, + "data[1]": 1157372931, + "data[2]": 1919744687, + "data[3]": 172923811 + } +} \ No newline at end of file diff --git a/tests/Fixtures/VolumetricLights/LICENSE b/tests/Fixtures/VolumetricLights/LICENSE new file mode 100644 index 0000000..da8818f --- /dev/null +++ b/tests/Fixtures/VolumetricLights/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2016, Michal Skalsky +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of VolumetricLights nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/tests/Fixtures/VolumetricLights/README.md b/tests/Fixtures/VolumetricLights/README.md new file mode 100644 index 0000000..58e71c3 --- /dev/null +++ b/tests/Fixtures/VolumetricLights/README.md @@ -0,0 +1,32 @@ +# VolumetricLights regression corpus + +Three shaders from [SlightlyMad/VolumetricLights](https://github.com/SlightlyMad/VolumetricLights/tree/d20b6f9a47ae857e1d313b147db2df3ef44b9196), distributed under the included BSD-3-Clause license. Unity Standard Assets are excluded. + +- `Source/`: unchanged upstream sources. +- `Imported/`: sources after Unity 6000.0.75f1's automatic shader migration. This migration replaces legacy Unity 5 identifiers and matrix expressions before compilation; it is not a decompiler output repair. +- `Json/`: unmodified AssetStudioModCLI 0.20.0.0 JSON dumps, renamed using their parsed shader names. +- `manifest.json`: provenance and SHA-256 hashes of all three representations. + +## Regeneration + +Use Windows, Unity CLI, Unity **6000.0.75f1**, a disposable Built-in project, and [AssetStudioModCLI 0.20.0](https://github.com/aelurum/AssetStudioMod/releases/tag/v0.20.0). Paths below are relative to this directory; executable locations may be supplied through PATH. + +1. Create the project with `unity projects create VolumetricLightFixtures --path . --editor-version 6000.0.75f1 --template com.unity.template.3d --no-cloud --non-interactive`. +2. Copy `Source/*.shader` into its `Assets/VolumetricLights/` and `BuildFixtures.cs` into `Assets/Editor/`. +3. Run `unity run ./VolumetricLightFixtures --editor-version 6000.0.75f1 --timeout 1800 --no-tail -- -executeMethod BuildFixtures.Run -force-d3d11`. Require exit code zero and `fixture-build.json` with `passed: true`. The build includes all three shaders, uses D3D11 only, and rejects shader compilation errors. +4. Review the imported source changes against `Imported/`. Do not disable migration: the original Unity 5 `_Object2World` identifier does not compile in this Editor. +5. Run `AssetStudioModCLI ./VolumetricLightFixtures/FixtureBundle/volumetriclights -m jsonDump -t shader -g none -f assetName -o ./exported`. Require exactly three exports; map their `m_ParsedForm.m_Name` to the matching filenames in `Json/`. +6. Review source/export differences and update the SHA-256 manifest explicitly. Never patch generated decompiled shaders. + +## Tests and current limitations + +From the repository root: + +```sh +dotnet build src/UnityShaderDecompiler.Cli/UnityShaderDecompiler.Cli.csproj -c Release +python tests/e2e/test_volumetric_lights.py +``` + +The test checks fixture integrity, D3D11-only inputs, extraction, CLI completion, absence of swallowed blob errors, and output shader identity/stages. It does **not** establish GPU rendering equivalence, complete runtime keyword selection, or successful recompilation of the generated HLSL. + +On upstream `417f26b`, all three extraction cases fail because the JSON contains `compressedBlob`, while the extractor requires `m_CompressedBlob`. PR #6 addresses schema compatibility. A diagnostic run using the compatible extractor also exposes `No DXBC: Unknown shader blob format` on upstream's Unity 6 blob reader. These failures are intentional regression signals, not skipped tests. Keep this PR in draft until the production fixes are available and the full cases pass. diff --git a/tests/Fixtures/VolumetricLights/Source/BilateralBlur.shader b/tests/Fixtures/VolumetricLights/Source/BilateralBlur.shader new file mode 100644 index 0000000..0b59683 --- /dev/null +++ b/tests/Fixtures/VolumetricLights/Source/BilateralBlur.shader @@ -0,0 +1,524 @@ +// Copyright(c) 2016, Michal Skalsky +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors +// may be used to endorse or promote products derived from this software without +// specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +// OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.IN NO EVENT +// SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT +// OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +// TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +// EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + +Shader "Hidden/BilateralBlur" +{ + Properties + { + _MainTex("Texture", any) = "" {} + } + SubShader + { + // No culling or depth + Cull Off ZWrite Off ZTest Always + + CGINCLUDE + + //-------------------------------------------------------------------------------------------- + // Downsample, bilateral blur and upsample config + //-------------------------------------------------------------------------------------------- + // method used to downsample depth buffer: 0 = min; 1 = max; 2 = min/max in chessboard pattern + #define DOWNSAMPLE_DEPTH_MODE 2 + #define UPSAMPLE_DEPTH_THRESHOLD 1.5f + #define BLUR_DEPTH_FACTOR 0.5 + #define GAUSS_BLUR_DEVIATION 1.5 + #define FULL_RES_BLUR_KERNEL_SIZE 7 + #define HALF_RES_BLUR_KERNEL_SIZE 5 + #define QUARTER_RES_BLUR_KERNEL_SIZE 6 + //-------------------------------------------------------------------------------------------- + + #define PI 3.1415927f + +#include "UnityCG.cginc" + + UNITY_DECLARE_TEX2D(_CameraDepthTexture); + UNITY_DECLARE_TEX2D(_HalfResDepthBuffer); + UNITY_DECLARE_TEX2D(_QuarterResDepthBuffer); + UNITY_DECLARE_TEX2D(_HalfResColor); + UNITY_DECLARE_TEX2D(_QuarterResColor); + UNITY_DECLARE_TEX2D(_MainTex); + + float4 _CameraDepthTexture_TexelSize; + float4 _HalfResDepthBuffer_TexelSize; + float4 _QuarterResDepthBuffer_TexelSize; + + struct appdata + { + float4 vertex : POSITION; + float2 uv : TEXCOORD0; + }; + + struct v2f + { + float2 uv : TEXCOORD0; + float4 vertex : SV_POSITION; + }; + + struct v2fDownsample + { +#if SHADER_TARGET > 40 + float2 uv : TEXCOORD0; +#else + float2 uv00 : TEXCOORD0; + float2 uv01 : TEXCOORD1; + float2 uv10 : TEXCOORD2; + float2 uv11 : TEXCOORD3; +#endif + float4 vertex : SV_POSITION; + }; + + struct v2fUpsample + { + float2 uv : TEXCOORD0; + float2 uv00 : TEXCOORD1; + float2 uv01 : TEXCOORD2; + float2 uv10 : TEXCOORD3; + float2 uv11 : TEXCOORD4; + float4 vertex : SV_POSITION; + }; + + v2f vert(appdata v) + { + v2f o; + o.vertex = mul(UNITY_MATRIX_MVP, v.vertex); + o.uv = v.uv; + return o; + } + + //----------------------------------------------------------------------------------------- + // vertDownsampleDepth + //----------------------------------------------------------------------------------------- + v2fDownsample vertDownsampleDepth(appdata v, float2 texelSize) + { + v2fDownsample o; + o.vertex = mul(UNITY_MATRIX_MVP, v.vertex); +#if SHADER_TARGET > 40 + o.uv = v.uv; +#else + o.uv00 = v.uv - 0.5 * texelSize.xy; + o.uv10 = o.uv00 + float2(texelSize.x, 0); + o.uv01 = o.uv00 + float2(0, texelSize.y); + o.uv11 = o.uv00 + texelSize.xy; +#endif + return o; + } + + //----------------------------------------------------------------------------------------- + // vertUpsample + //----------------------------------------------------------------------------------------- + v2fUpsample vertUpsample(appdata v, float2 texelSize) + { + v2fUpsample o; + o.vertex = mul(UNITY_MATRIX_MVP, v.vertex); + o.uv = v.uv; + + o.uv00 = v.uv - 0.5 * texelSize.xy; + o.uv10 = o.uv00 + float2(texelSize.x, 0); + o.uv01 = o.uv00 + float2(0, texelSize.y); + o.uv11 = o.uv00 + texelSize.xy; + return o; + } + + //----------------------------------------------------------------------------------------- + // BilateralUpsample + //----------------------------------------------------------------------------------------- + float4 BilateralUpsample(v2fUpsample input, Texture2D hiDepth, Texture2D loDepth, Texture2D loColor, SamplerState linearSampler, SamplerState pointSampler) + { + const float threshold = UPSAMPLE_DEPTH_THRESHOLD; + float4 highResDepth = LinearEyeDepth(hiDepth.Sample(pointSampler, input.uv)).xxxx; + float4 lowResDepth; + + lowResDepth[0] = LinearEyeDepth(loDepth.Sample(pointSampler, input.uv00)); + lowResDepth[1] = LinearEyeDepth(loDepth.Sample(pointSampler, input.uv10)); + lowResDepth[2] = LinearEyeDepth(loDepth.Sample(pointSampler, input.uv01)); + lowResDepth[3] = LinearEyeDepth(loDepth.Sample(pointSampler, input.uv11)); + + float4 depthDiff = abs(lowResDepth - highResDepth); + + float accumDiff = dot(depthDiff, float4(1, 1, 1, 1)); + + [branch] + if (accumDiff < threshold) // small error, not an edge -> use bilinear filter + { + return loColor.Sample(linearSampler, input.uv); + } + + // find nearest sample + float minDepthDiff = depthDiff[0]; + float2 nearestUv = input.uv00; + + if (depthDiff[1] < minDepthDiff) + { + nearestUv = input.uv10; + minDepthDiff = depthDiff[1]; + } + + if (depthDiff[2] < minDepthDiff) + { + nearestUv = input.uv01; + minDepthDiff = depthDiff[2]; + } + + if (depthDiff[3] < minDepthDiff) + { + nearestUv = input.uv11; + minDepthDiff = depthDiff[3]; + } + + return loColor.Sample(pointSampler, nearestUv); + } + + //----------------------------------------------------------------------------------------- + // DownsampleDepth + //----------------------------------------------------------------------------------------- + float DownsampleDepth(v2fDownsample input, Texture2D depthTexture, SamplerState depthSampler) + { +#if SHADER_TARGET > 40 + float4 depth = depthTexture.Gather(depthSampler, input.uv); +#else + float4 depth; + depth.x = depthTexture.Sample(depthSampler, input.uv00).x; + depth.y = depthTexture.Sample(depthSampler, input.uv01).x; + depth.z = depthTexture.Sample(depthSampler, input.uv10).x; + depth.w = depthTexture.Sample(depthSampler, input.uv11).x; + +#endif + +#if DOWNSAMPLE_DEPTH_MODE == 0 // min depth + return min(min(depth.x, depth.y), min(depth.z, depth.w)); +#elif DOWNSAMPLE_DEPTH_MODE == 1 // max depth + return max(max(depth.x, depth.y), max(depth.z, depth.w)); +#elif DOWNSAMPLE_DEPTH_MODE == 2 // min/max depth in chessboard pattern + + float minDepth = min(min(depth.x, depth.y), min(depth.z, depth.w)); + float maxDepth = max(max(depth.x, depth.y), max(depth.z, depth.w)); + + // chessboard pattern + int2 position = input.vertex.xy % 2; + int index = position.x + position.y; + return index == 1 ? minDepth : maxDepth; +#endif + } + + //----------------------------------------------------------------------------------------- + // GaussianWeight + //----------------------------------------------------------------------------------------- + float GaussianWeight(float offset, float deviation) + { + float weight = 1.0f / sqrt(2.0f * PI * deviation * deviation); + weight *= exp(-(offset * offset) / (2.0f * deviation * deviation)); + return weight; + } + + //----------------------------------------------------------------------------------------- + // BilateralBlur + //----------------------------------------------------------------------------------------- + float4 BilateralBlur(v2f input, int2 direction, Texture2D depth, SamplerState depthSampler, const int kernelRadius, float2 pixelSize) + { + //const float deviation = kernelRadius / 2.5; + const float deviation = kernelRadius / GAUSS_BLUR_DEVIATION; // make it really strong + + float2 uv = input.uv; + float4 centerColor = _MainTex.Sample(sampler_MainTex, uv); + float3 color = centerColor.xyz; + //return float4(color, 1); + float centerDepth = (LinearEyeDepth(depth.Sample(depthSampler, uv))); + + float weightSum = 0; + + // gaussian weight is computed from constants only -> will be computed in compile time + float weight = GaussianWeight(0, deviation); + color *= weight; + weightSum += weight; + + [unroll] for (int i = -kernelRadius; i < 0; i += 1) + { + float2 offset = (direction * i); + float3 sampleColor = _MainTex.Sample(sampler_MainTex, input.uv, offset); + float sampleDepth = (LinearEyeDepth(depth.Sample(depthSampler, input.uv, offset))); + + float depthDiff = abs(centerDepth - sampleDepth); + float dFactor = depthDiff * BLUR_DEPTH_FACTOR; + float w = exp(-(dFactor * dFactor)); + + // gaussian weight is computed from constants only -> will be computed in compile time + weight = GaussianWeight(i, deviation) * w; + + color += weight * sampleColor; + weightSum += weight; + } + + [unroll] for (i = 1; i <= kernelRadius; i += 1) + { + float2 offset = (direction * i); + float3 sampleColor = _MainTex.Sample(sampler_MainTex, input.uv, offset); + float sampleDepth = (LinearEyeDepth(depth.Sample(depthSampler, input.uv, offset))); + + float depthDiff = abs(centerDepth - sampleDepth); + float dFactor = depthDiff * BLUR_DEPTH_FACTOR; + float w = exp(-(dFactor * dFactor)); + + // gaussian weight is computed from constants only -> will be computed in compile time + weight = GaussianWeight(i, deviation) * w; + + color += weight * sampleColor; + weightSum += weight; + } + + color /= weightSum; + return float4(color, centerColor.w); + } + + ENDCG + + // pass 0 - horizontal blur (hires) + Pass + { + CGPROGRAM + #pragma vertex vert + #pragma fragment horizontalFrag + #pragma target 4.0 + + fixed4 horizontalFrag(v2f input) : SV_Target + { + return BilateralBlur(input, int2(1, 0), _CameraDepthTexture, sampler_CameraDepthTexture, FULL_RES_BLUR_KERNEL_SIZE, _CameraDepthTexture_TexelSize.xy); + } + + ENDCG + } + + // pass 1 - vertical blur (hires) + Pass + { + CGPROGRAM + #pragma vertex vert + #pragma fragment verticalFrag + #pragma target 4.0 + + fixed4 verticalFrag(v2f input) : SV_Target + { + return BilateralBlur(input, int2(0, 1), _CameraDepthTexture, sampler_CameraDepthTexture, FULL_RES_BLUR_KERNEL_SIZE, _CameraDepthTexture_TexelSize.xy); + } + + ENDCG + } + + // pass 2 - horizontal blur (lores) + Pass + { + CGPROGRAM + #pragma vertex vert + #pragma fragment horizontalFrag + #pragma target 4.0 + + fixed4 horizontalFrag(v2f input) : SV_Target + { + return BilateralBlur(input, int2(1, 0), _HalfResDepthBuffer, sampler_HalfResDepthBuffer, HALF_RES_BLUR_KERNEL_SIZE, _HalfResDepthBuffer_TexelSize.xy); + } + + ENDCG + } + + // pass 3 - vertical blur (lores) + Pass + { + CGPROGRAM + #pragma vertex vert + #pragma fragment verticalFrag + #pragma target 4.0 + + fixed4 verticalFrag(v2f input) : SV_Target + { + return BilateralBlur(input, int2(0, 1), _HalfResDepthBuffer, sampler_HalfResDepthBuffer, HALF_RES_BLUR_KERNEL_SIZE, _HalfResDepthBuffer_TexelSize.xy); + } + + ENDCG + } + + // pass 4 - downsample depth to half + Pass + { + CGPROGRAM + #pragma vertex vertHalfDepth + #pragma fragment frag + #pragma target gl4.1 + + v2fDownsample vertHalfDepth(appdata v) + { + return vertDownsampleDepth(v, _CameraDepthTexture_TexelSize); + } + + float frag(v2fDownsample input) : SV_Target + { + return DownsampleDepth(input, _CameraDepthTexture, sampler_CameraDepthTexture); + } + + ENDCG + } + + // pass 5 - bilateral upsample + Pass + { + Blend One Zero + + CGPROGRAM + #pragma vertex vertUpsampleToFull + #pragma fragment frag + #pragma target 4.0 + + v2fUpsample vertUpsampleToFull(appdata v) + { + return vertUpsample(v, _HalfResDepthBuffer_TexelSize); + } + float4 frag(v2fUpsample input) : SV_Target + { + return BilateralUpsample(input, _CameraDepthTexture, _HalfResDepthBuffer, _HalfResColor, sampler_HalfResColor, sampler_HalfResDepthBuffer); + } + + ENDCG + } + + // pass 6 - downsample depth to quarter + Pass + { + CGPROGRAM + #pragma vertex vertQuarterDepth + #pragma fragment frag + #pragma target gl4.1 + + v2fDownsample vertQuarterDepth(appdata v) + { + return vertDownsampleDepth(v, _HalfResDepthBuffer_TexelSize); + } + + float frag(v2fDownsample input) : SV_Target + { + return DownsampleDepth(input, _HalfResDepthBuffer, sampler_HalfResDepthBuffer); + } + + ENDCG + } + + // pass 7 - bilateral upsample quarter to full + Pass + { + Blend One Zero + + CGPROGRAM + #pragma vertex vertUpsampleToFull + #pragma fragment frag + #pragma target 4.0 + + v2fUpsample vertUpsampleToFull(appdata v) + { + return vertUpsample(v, _QuarterResDepthBuffer_TexelSize); + } + float4 frag(v2fUpsample input) : SV_Target + { + return BilateralUpsample(input, _CameraDepthTexture, _QuarterResDepthBuffer, _QuarterResColor, sampler_QuarterResColor, sampler_QuarterResDepthBuffer); + } + + ENDCG + } + + // pass 8 - horizontal blur (quarter res) + Pass + { + CGPROGRAM + #pragma vertex vert + #pragma fragment horizontalFrag + #pragma target 4.0 + + fixed4 horizontalFrag(v2f input) : SV_Target + { + return BilateralBlur(input, int2(1, 0), _QuarterResDepthBuffer, sampler_QuarterResDepthBuffer, QUARTER_RES_BLUR_KERNEL_SIZE, _QuarterResDepthBuffer_TexelSize.xy); + } + + ENDCG + } + + // pass 9 - vertical blur (quarter res) + Pass + { + CGPROGRAM + #pragma vertex vert + #pragma fragment verticalFrag + #pragma target 4.0 + + fixed4 verticalFrag(v2f input) : SV_Target + { + return BilateralBlur(input, int2(0, 1), _QuarterResDepthBuffer, sampler_QuarterResDepthBuffer, QUARTER_RES_BLUR_KERNEL_SIZE, _QuarterResDepthBuffer_TexelSize.xy); + } + + ENDCG + } + + // pass 10 - downsample depth to half (fallback for DX10) + Pass + { + CGPROGRAM + #pragma vertex vertHalfDepth + #pragma fragment frag + #pragma target 4.0 + + v2fDownsample vertHalfDepth(appdata v) + { + return vertDownsampleDepth(v, _CameraDepthTexture_TexelSize); + } + + float frag(v2fDownsample input) : SV_Target + { + return DownsampleDepth(input, _CameraDepthTexture, sampler_CameraDepthTexture); + } + + ENDCG + } + + // pass 11 - downsample depth to quarter (fallback for DX10) + Pass + { + CGPROGRAM + #pragma vertex vertQuarterDepth + #pragma fragment frag + #pragma target 4.0 + + v2fDownsample vertQuarterDepth(appdata v) + { + return vertDownsampleDepth(v, _HalfResDepthBuffer_TexelSize); + } + + float frag(v2fDownsample input) : SV_Target + { + return DownsampleDepth(input, _HalfResDepthBuffer, sampler_HalfResDepthBuffer); + } + + ENDCG + } + } +} diff --git a/tests/Fixtures/VolumetricLights/Source/BlitAdd.shader b/tests/Fixtures/VolumetricLights/Source/BlitAdd.shader new file mode 100644 index 0000000..8232070 --- /dev/null +++ b/tests/Fixtures/VolumetricLights/Source/BlitAdd.shader @@ -0,0 +1,90 @@ +// Copyright(c) 2016, Michal Skalsky +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors +// may be used to endorse or promote products derived from this software without +// specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +// OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.IN NO EVENT +// SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT +// OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +// TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +// EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + +Shader "Hidden/BlitAdd" +{ + Properties{ _MainTex("Texture", any) = "" {} } + SubShader + { + Pass + { + ZTest Always Cull Off ZWrite Off + Blend One Zero + + CGPROGRAM + #pragma vertex vert + #pragma fragment frag + + #include "UnityCG.cginc" + + sampler2D _MainTex; + sampler2D _Source; + uniform float4 _MainTex_ST; + float4 _Source_TexelSize; + + struct appdata_t + { + float4 vertex : POSITION; + float2 texcoord : TEXCOORD0; + }; + + struct v2f + { + float4 vertex : SV_POSITION; + float2 texcoord : TEXCOORD0; + }; + + v2f vert(appdata_t v) + { + v2f o; + o.vertex = mul(UNITY_MATRIX_MVP, v.vertex); + o.texcoord = TRANSFORM_TEX(v.texcoord.xy, _MainTex); + + return o; + } + + fixed4 frag(v2f i) : SV_Target + { + float4 main = tex2D(_MainTex, i.texcoord); + +#if UNITY_UV_STARTS_AT_TOP + if (_Source_TexelSize.y < 0) + i.texcoord.y = 1 - i.texcoord.y; +#endif + float4 source = tex2D(_Source, i.texcoord); + + source *= main.w; + source.xyz += main.xyz; + return source; + } + ENDCG + } + } + Fallback Off +} diff --git a/tests/Fixtures/VolumetricLights/Source/VolumetricLight.shader b/tests/Fixtures/VolumetricLights/Source/VolumetricLight.shader new file mode 100644 index 0000000..ead4d6e --- /dev/null +++ b/tests/Fixtures/VolumetricLights/Source/VolumetricLight.shader @@ -0,0 +1,644 @@ +// Copyright(c) 2016, Michal Skalsky +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors +// may be used to endorse or promote products derived from this software without +// specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +// OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.IN NO EVENT +// SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT +// OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +// TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +// EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + +Shader "Sandbox/VolumetricLight" +{ + Properties + { + [HideInInspector]_MainTex ("Texture", 2D) = "white" {} + [HideInInspector]_ZTest ("ZTest", Float) = 0 + [HideInInspector]_LightColor("_LightColor", Color) = (1,1,1,1) + } + SubShader + { + Tags { "RenderType"="Opaque" } + LOD 100 + + CGINCLUDE + + #if defined(SHADOWS_DEPTH) || defined(SHADOWS_CUBE) + #define SHADOWS_NATIVE + #endif + + #include "UnityCG.cginc" + #include "UnityDeferredLibrary.cginc" + + sampler3D _NoiseTexture; + sampler2D _DitherTexture; + + float4 _FrustumCorners[4]; + + struct appdata + { + float4 vertex : POSITION; + }; + + float4x4 _WorldViewProj; + float4x4 _MyLightMatrix0; + float4x4 _MyWorld2Shadow; + + float3 _CameraForward; + + // x: scattering coef, y: extinction coef, z: range w: skybox extinction coef + float4 _VolumetricLight; + // x: 1 - g^2, y: 1 + g^2, z: 2*g, w: 1/4pi + float4 _MieG; + + // x: scale, y: intensity, z: intensity offset + float4 _NoiseData; + // x: x velocity, y: z velocity + float4 _NoiseVelocity; + // x: ground level, y: height scale, z: unused, w: unused + float4 _HeightFog; + //float4 _LightDir; + + float _MaxRayLength; + + int _SampleCount; + + struct v2f + { + float4 pos : SV_POSITION; + float4 uv : TEXCOORD0; + float3 wpos : TEXCOORD1; + }; + + v2f vert(appdata v) + { + v2f o; + o.pos = mul(_WorldViewProj, v.vertex); + o.uv = ComputeScreenPos(o.pos); + o.wpos = mul(_Object2World, v.vertex); + return o; + } + + //----------------------------------------------------------------------------------------- + // GetCascadeWeights_SplitSpheres + //----------------------------------------------------------------------------------------- + inline fixed4 GetCascadeWeights_SplitSpheres(float3 wpos) + { + float3 fromCenter0 = wpos.xyz - unity_ShadowSplitSpheres[0].xyz; + float3 fromCenter1 = wpos.xyz - unity_ShadowSplitSpheres[1].xyz; + float3 fromCenter2 = wpos.xyz - unity_ShadowSplitSpheres[2].xyz; + float3 fromCenter3 = wpos.xyz - unity_ShadowSplitSpheres[3].xyz; + float4 distances2 = float4(dot(fromCenter0, fromCenter0), dot(fromCenter1, fromCenter1), dot(fromCenter2, fromCenter2), dot(fromCenter3, fromCenter3)); + + fixed4 weights = float4(distances2 < unity_ShadowSplitSqRadii); + weights.yzw = saturate(weights.yzw - weights.xyz); + return weights; + } + + //----------------------------------------------------------------------------------------- + // GetCascadeShadowCoord + //----------------------------------------------------------------------------------------- + inline float4 GetCascadeShadowCoord(float4 wpos, fixed4 cascadeWeights) + { + float3 sc0 = mul(unity_World2Shadow[0], wpos).xyz; + float3 sc1 = mul(unity_World2Shadow[1], wpos).xyz; + float3 sc2 = mul(unity_World2Shadow[2], wpos).xyz; + float3 sc3 = mul(unity_World2Shadow[3], wpos).xyz; + + float4 shadowMapCoordinate = float4(sc0 * cascadeWeights[0] + sc1 * cascadeWeights[1] + sc2 * cascadeWeights[2] + sc3 * cascadeWeights[3], 1); +#if defined(UNITY_REVERSED_Z) + float noCascadeWeights = 1 - dot(cascadeWeights, float4(1, 1, 1, 1)); + shadowMapCoordinate.z += noCascadeWeights; +#endif + return shadowMapCoordinate; + } + + UNITY_DECLARE_SHADOWMAP(_CascadeShadowMapTexture); + + //----------------------------------------------------------------------------------------- + // GetLightAttenuation + //----------------------------------------------------------------------------------------- + float GetLightAttenuation(float3 wpos) + { + float atten = 0; +#if defined (DIRECTIONAL) || defined (DIRECTIONAL_COOKIE) + atten = 1; +#if defined (SHADOWS_DEPTH) + // sample cascade shadow map + float4 cascadeWeights = GetCascadeWeights_SplitSpheres(wpos); + bool inside = dot(cascadeWeights, float4(1, 1, 1, 1)) < 4; + float4 samplePos = GetCascadeShadowCoord(float4(wpos, 1), cascadeWeights); + + atten = inside ? UNITY_SAMPLE_SHADOW(_CascadeShadowMapTexture, samplePos.xyz) : 1.0f; + atten = _LightShadowData.r + atten * (1 - _LightShadowData.r); + //atten = inside ? tex2Dproj(_ShadowMapTexture, float4((samplePos).xyz, 1)).r : 1.0f; +#endif +#if defined (DIRECTIONAL_COOKIE) + // NOT IMPLEMENTED +#endif +#elif defined (SPOT) + float3 tolight = _LightPos.xyz - wpos; + half3 lightDir = normalize(tolight); + + float4 uvCookie = mul(_MyLightMatrix0, float4(wpos, 1)); + // negative bias because http://aras-p.info/blog/2010/01/07/screenspace-vs-mip-mapping/ + atten = tex2Dbias(_LightTexture0, float4(uvCookie.xy / uvCookie.w, 0, -8)).w; + atten *= uvCookie.w < 0; + float att = dot(tolight, tolight) * _LightPos.w; + atten *= tex2D(_LightTextureB0, att.rr).UNITY_ATTEN_CHANNEL; + +#if defined(SHADOWS_DEPTH) + float4 shadowCoord = mul(_MyWorld2Shadow, float4(wpos, 1)); + atten *= saturate(UnitySampleShadowmap(shadowCoord)); +#endif +#elif defined (POINT) || defined (POINT_COOKIE) + float3 tolight = wpos - _LightPos.xyz; + half3 lightDir = -normalize(tolight); + + float att = dot(tolight, tolight) * _LightPos.w; + atten = tex2D(_LightTextureB0, att.rr).UNITY_ATTEN_CHANNEL; + + atten *= UnityDeferredComputeShadow(tolight, 0, float2(0, 0)); + +#if defined (POINT_COOKIE) + atten *= texCUBEbias(_LightTexture0, float4(mul(_MyLightMatrix0, half4(wpos, 1)).xyz, -8)).w; +#endif //POINT_COOKIE +#endif + return atten; + } + + //----------------------------------------------------------------------------------------- + // ApplyHeightFog + //----------------------------------------------------------------------------------------- + void ApplyHeightFog(float3 wpos, inout float density) + { +#ifdef HEIGHT_FOG + density *= exp(-(wpos.y + _HeightFog.x) * _HeightFog.y); +#endif + } + + //----------------------------------------------------------------------------------------- + // GetDensity + //----------------------------------------------------------------------------------------- + float GetDensity(float3 wpos) + { + float density = 1; +#ifdef NOISE + float noise = tex3D(_NoiseTexture, frac(wpos * _NoiseData.x + float3(_Time.y * _NoiseVelocity.x, 0, _Time.y * _NoiseVelocity.y))); + noise = saturate(noise - _NoiseData.z) * _NoiseData.y; + density = saturate(noise); +#endif + ApplyHeightFog(wpos, density); + + return density; + } + + //----------------------------------------------------------------------------------------- + // MieScattering + //----------------------------------------------------------------------------------------- + float MieScattering(float cosAngle, float4 g) + { + return g.w * (g.x / (pow(g.y - g.z * cosAngle, 1.5))); + } + + //----------------------------------------------------------------------------------------- + // RayMarch + //----------------------------------------------------------------------------------------- + float4 RayMarch(float2 screenPos, float3 rayStart, float3 rayDir, float rayLength) + { +#ifdef DITHER_4_4 + float2 interleavedPos = (fmod(floor(screenPos.xy), 4.0)); + float offset = tex2D(_DitherTexture, interleavedPos / 4.0 + float2(0.5 / 4.0, 0.5 / 4.0)).w; +#else + float2 interleavedPos = (fmod(floor(screenPos.xy), 8.0)); + float offset = tex2D(_DitherTexture, interleavedPos / 8.0 + float2(0.5 / 8.0, 0.5 / 8.0)).w; +#endif + + int stepCount = _SampleCount; + + float stepSize = rayLength / stepCount; + float3 step = rayDir * stepSize; + + float3 currentPosition = rayStart + step * offset; + + float4 vlight = 0; + + float cosAngle; +#if defined (DIRECTIONAL) || defined (DIRECTIONAL_COOKIE) + float extinction = 0; + cosAngle = dot(_LightDir.xyz, -rayDir); +#else + // we don't know about density between camera and light's volume, assume 0.5 + float extinction = length(_WorldSpaceCameraPos - currentPosition) * _VolumetricLight.y * 0.5; +#endif + [loop] + for (int i = 0; i < stepCount; ++i) + { + float atten = GetLightAttenuation(currentPosition); + float density = GetDensity(currentPosition); + + float scattering = _VolumetricLight.x * stepSize * density; + extinction += _VolumetricLight.y * stepSize * density;// +scattering; + + float4 light = atten * scattering * exp(-extinction); + +//#if PHASE_FUNCTOIN +#if !defined (DIRECTIONAL) && !defined (DIRECTIONAL_COOKIE) + // phase functino for spot and point lights + float3 tolight = normalize(currentPosition - _LightPos.xyz); + cosAngle = dot(tolight, -rayDir); + light *= MieScattering(cosAngle, _MieG); +#endif +//#endif + vlight += light; + + currentPosition += step; + } + +#if defined (DIRECTIONAL) || defined (DIRECTIONAL_COOKIE) + // apply phase function for dir light + vlight *= MieScattering(cosAngle, _MieG); +#endif + + // apply light's color + vlight *= _LightColor; + + vlight = max(0, vlight); +#if defined (DIRECTIONAL) || defined (DIRECTIONAL_COOKIE) // use "proper" out-scattering/absorption for dir light + vlight.w = exp(-extinction); +#else + vlight.w = 0; +#endif + return vlight; + } + + //----------------------------------------------------------------------------------------- + // RayConeIntersect + //----------------------------------------------------------------------------------------- + float2 RayConeIntersect(in float3 f3ConeApex, in float3 f3ConeAxis, in float fCosAngle, in float3 f3RayStart, in float3 f3RayDir) + { + float inf = 10000; + f3RayStart -= f3ConeApex; + float a = dot(f3RayDir, f3ConeAxis); + float b = dot(f3RayDir, f3RayDir); + float c = dot(f3RayStart, f3ConeAxis); + float d = dot(f3RayStart, f3RayDir); + float e = dot(f3RayStart, f3RayStart); + fCosAngle *= fCosAngle; + float A = a*a - b*fCosAngle; + float B = 2 * (c*a - d*fCosAngle); + float C = c*c - e*fCosAngle; + float D = B*B - 4 * A*C; + + if (D > 0) + { + D = sqrt(D); + float2 t = (-B + sign(A)*float2(-D, +D)) / (2 * A); + bool2 b2IsCorrect = c + a * t > 0 && t > 0; + t = t * b2IsCorrect + !b2IsCorrect * (inf); + return t; + } + else // no intersection + return inf; + } + + //----------------------------------------------------------------------------------------- + // RayPlaneIntersect + //----------------------------------------------------------------------------------------- + float RayPlaneIntersect(in float3 planeNormal, in float planeD, in float3 rayOrigin, in float3 rayDir) + { + float NdotD = dot(planeNormal, rayDir); + float NdotO = dot(planeNormal, rayOrigin); + + float t = -(NdotO + planeD) / NdotD; + if (t < 0) + t = 100000; + return t; + } + + ENDCG + + // pass 0 - point light, camera inside + Pass + { + ZTest Off + Cull Front + ZWrite Off + Blend One One + + CGPROGRAM + #pragma vertex vert + #pragma fragment fragPointInside + #pragma target 4.0 + + #define UNITY_HDR_ON + + #pragma shader_feature HEIGHT_FOG + #pragma shader_feature NOISE + #pragma shader_feature SHADOWS_CUBE + #pragma shader_feature POINT_COOKIE + #pragma shader_feature POINT + + #ifdef SHADOWS_DEPTH + #define SHADOWS_NATIVE + #endif + + + fixed4 fragPointInside(v2f i) : SV_Target + { + float2 uv = i.uv.xy / i.uv.w; + + // read depth and reconstruct world position + float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv); + + float3 rayStart = _WorldSpaceCameraPos; + float3 rayEnd = i.wpos; + + float3 rayDir = (rayEnd - rayStart); + float rayLength = length(rayDir); + + rayDir /= rayLength; + + float linearDepth = LinearEyeDepth(depth); + float projectedDepth = linearDepth / dot(_CameraForward, rayDir); + rayLength = min(rayLength, projectedDepth); + + return RayMarch(i.pos.xy, rayStart, rayDir, rayLength); + } + ENDCG + } + + // pass 1 - spot light, camera inside + Pass + { + ZTest Off + Cull Front + ZWrite Off + Blend One One + + CGPROGRAM +#pragma vertex vert +#pragma fragment fragPointInside +#pragma target 4.0 + +#define UNITY_HDR_ON + +#pragma shader_feature HEIGHT_FOG +#pragma shader_feature NOISE +#pragma shader_feature SHADOWS_DEPTH +#pragma shader_feature SPOT + +#ifdef SHADOWS_DEPTH +#define SHADOWS_NATIVE +#endif + + fixed4 fragPointInside(v2f i) : SV_Target + { + float2 uv = i.uv.xy / i.uv.w; + + // read depth and reconstruct world position + float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv); + + float3 rayStart = _WorldSpaceCameraPos; + float3 rayEnd = i.wpos; + + float3 rayDir = (rayEnd - rayStart); + float rayLength = length(rayDir); + + rayDir /= rayLength; + + float linearDepth = LinearEyeDepth(depth); + float projectedDepth = linearDepth / dot(_CameraForward, rayDir); + rayLength = min(rayLength, projectedDepth); + + return RayMarch(i.pos.xy, rayStart, rayDir, rayLength); + } + ENDCG + } + + // pass 2 - point light, camera outside + Pass + { + //ZTest Off + ZTest [_ZTest] + Cull Back + ZWrite Off + Blend One One + + CGPROGRAM + #pragma vertex vert + #pragma fragment fragPointOutside + #pragma target 4.0 + + #define UNITY_HDR_ON + + #pragma shader_feature HEIGHT_FOG + #pragma shader_feature SHADOWS_CUBE + #pragma shader_feature NOISE + //#pragma multi_compile POINT POINT_COOKIE + #pragma shader_feature POINT_COOKIE + #pragma shader_feature POINT + + fixed4 fragPointOutside(v2f i) : SV_Target + { + float2 uv = i.uv.xy / i.uv.w; + + // read depth and reconstruct world position + float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv); + + float3 rayStart = _WorldSpaceCameraPos; + float3 rayEnd = i.wpos; + + float3 rayDir = (rayEnd - rayStart); + float rayLength = length(rayDir); + + rayDir /= rayLength; + + float3 lightToCamera = _WorldSpaceCameraPos - _LightPos; + + float b = dot(rayDir, lightToCamera); + float c = dot(lightToCamera, lightToCamera) - (_VolumetricLight.z * _VolumetricLight.z); + + float d = sqrt((b*b) - c); + float start = -b - d; + float end = -b + d; + + float linearDepth = LinearEyeDepth(depth); + float projectedDepth = linearDepth / dot(_CameraForward, rayDir); + end = min(end, projectedDepth); + + rayStart = rayStart + rayDir * start; + rayLength = end - start; + + return RayMarch(i.pos.xy, rayStart, rayDir, rayLength); + } + ENDCG + } + + // pass 3 - spot light, camera outside + Pass + { + //ZTest Off + ZTest[_ZTest] + Cull Back + ZWrite Off + Blend One One + + CGPROGRAM + #pragma vertex vert + #pragma fragment fragSpotOutside + #pragma target 4.0 + + #define UNITY_HDR_ON + + #pragma shader_feature HEIGHT_FOG + #pragma shader_feature SHADOWS_DEPTH + #pragma shader_feature NOISE + #pragma shader_feature SPOT + + #ifdef SHADOWS_DEPTH + #define SHADOWS_NATIVE + #endif + + float _CosAngle; + float4 _ConeAxis; + float4 _ConeApex; + float _PlaneD; + + fixed4 fragSpotOutside(v2f i) : SV_Target + { + float2 uv = i.uv.xy / i.uv.w; + + // read depth and reconstruct world position + float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv); + + float3 rayStart = _WorldSpaceCameraPos; + float3 rayEnd = i.wpos; + + float3 rayDir = (rayEnd - rayStart); + float rayLength = length(rayDir); + + rayDir /= rayLength; + + + // inside cone + float3 r1 = rayEnd + rayDir * 0.001; + + // plane intersection + float planeCoord = RayPlaneIntersect(_ConeAxis, _PlaneD, r1, rayDir); + // ray cone intersection + float2 lineCoords = RayConeIntersect(_ConeApex, _ConeAxis, _CosAngle, r1, rayDir); + + float linearDepth = LinearEyeDepth(depth); + float projectedDepth = linearDepth / dot(_CameraForward, rayDir); + + float z = (projectedDepth - rayLength); + rayLength = min(planeCoord, min(lineCoords.x, lineCoords.y)); + rayLength = min(rayLength, z); + + return RayMarch(i.pos.xy, rayEnd, rayDir, rayLength); + } + ENDCG + } + + // pass 4 - directional light + Pass + { + ZTest Off + Cull Off + ZWrite Off + Blend One One, One Zero + + CGPROGRAM + + #pragma vertex vertDir + #pragma fragment fragDir + #pragma target 4.0 + + #define UNITY_HDR_ON + + #pragma shader_feature HEIGHT_FOG + #pragma shader_feature NOISE + #pragma shader_feature SHADOWS_DEPTH + #pragma shader_feature DIRECTIONAL_COOKIE + #pragma shader_feature DIRECTIONAL + + #ifdef SHADOWS_DEPTH + #define SHADOWS_NATIVE + #endif + + struct VSInput + { + float4 vertex : POSITION; + float2 uv : TEXCOORD0; + uint vertexId : SV_VertexID; + }; + + struct PSInput + { + float4 pos : SV_POSITION; + float2 uv : TEXCOORD0; + float3 wpos : TEXCOORD1; + }; + + PSInput vertDir(VSInput i) + { + PSInput o; + + o.pos = mul(UNITY_MATRIX_MVP, i.vertex); + o.uv = i.uv; + + // SV_VertexId doesn't work on OpenGL for some reason -> reconstruct id from uv + //o.wpos = _FrustumCorners[i.vertexId]; + o.wpos = _FrustumCorners[i.uv.x + i.uv.y*2]; + + return o; + } + + fixed4 fragDir(PSInput i) : SV_Target + { + float2 uv = i.uv.xy; + float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv); + float linearDepth = Linear01Depth(depth); + + float3 wpos = i.wpos; + float3 rayStart = _WorldSpaceCameraPos; + float3 rayDir = wpos - _WorldSpaceCameraPos; + rayDir *= linearDepth; + + float rayLength = length(rayDir); + rayDir /= rayLength; + + rayLength = min(rayLength, _MaxRayLength); + + float4 color = RayMarch(i.pos.xy, rayStart, rayDir, rayLength); + + if (linearDepth > 0.999999) + { + color.w = lerp(color.w, 1, _VolumetricLight.w); + } + return color; + } + ENDCG + } + } +} diff --git a/tests/Fixtures/VolumetricLights/manifest.json b/tests/Fixtures/VolumetricLights/manifest.json new file mode 100644 index 0000000..3283094 --- /dev/null +++ b/tests/Fixtures/VolumetricLights/manifest.json @@ -0,0 +1,18 @@ +{ + "repository": "https://github.com/SlightlyMad/VolumetricLights", + "commit": "d20b6f9a47ae857e1d313b147db2df3ef44b9196", + "unity": "6000.0.75f1", + "graphicsApi": "Direct3D11", + "exporter": "AssetStudioModCLI 0.20.0.0", + "files": { + "Source/BilateralBlur.shader": "ed3cd194d27be303e0a648a04685c01622bc02e6cca7d8c3bea27fbc126656bd", + "Source/BlitAdd.shader": "d783e52f62494db7ec3ec656d6e90181c65a54a9072bc273f71d185ecd796784", + "Source/VolumetricLight.shader": "6aad6c38a4256471d3a4bf1704c3192afb01d66c353540822f624b1173dab973", + "Imported/BilateralBlur.shader": "7adff3672a1093c11634b53a7c4d5bf2a33c2c71a19c1c91acc88be431b7a38a", + "Imported/BlitAdd.shader": "361f7c1d1af13d8df3751df9d1af484a6d010bc06e65db2635380ea694bc2f0b", + "Imported/VolumetricLight.shader": "119aad3a4219cad09ab12b853bae12b049b90a56f7e0bc7340c9edc2bb27438f", + "Json/BilateralBlur.json": "af791b0a9655645699825e2a3f40a14828decf6c090a12ae5c666ecaa0472600", + "Json/BlitAdd.json": "39a02d21c946dac77ac8d41776b6dd7c2386ae76ee5b818907c442626ee0e177", + "Json/VolumetricLight.json": "faaec0af7cc376de79c112dd1658ed586b719ce385bdc821342b1aa2bc630132" + } +} diff --git a/tests/e2e/test_volumetric_lights.py b/tests/e2e/test_volumetric_lights.py new file mode 100644 index 0000000..34cf3fd --- /dev/null +++ b/tests/e2e/test_volumetric_lights.py @@ -0,0 +1,57 @@ +"""Offline regression corpus; build the Release CLI before running this file.""" +import hashlib +import json +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +FIXTURES = ROOT / "tests/Fixtures/VolumetricLights" +CLI = ROOT / "src/UnityShaderDecompiler.Cli/bin/Release/net10.0/Shader Decompiler.dll" + + +class VolumetricLightsTests(unittest.TestCase): + def test_fixture_integrity(self): + manifest = json.loads((FIXTURES / "manifest.json").read_text()) + for name, expected in manifest["files"].items(): + with self.subTest(file=name): + self.assertEqual(expected, hashlib.sha256((FIXTURES / name).read_bytes()).hexdigest()) + + def check_shader(self, name): + source = FIXTURES / "Json" / (name + ".json") + data = json.loads(source.read_text(encoding="utf-8-sig")) + self.assertEqual([4], data["platforms"], "Fixture must contain only D3D11") + self.assertTrue(CLI.is_file(), "Build the Release CLI first") + with tempfile.TemporaryDirectory(prefix="volumetric-lights-") as temporary: + folder = Path(temporary) + self.run_checked([sys.executable, str(ROOT / "Extract.py"), str(source), + "--flat", "--out-dir", str(folder / "input")]) + log = self.run_checked(["dotnet", str(CLI), str(folder / "input"), + "--no-fuse-temps", "--out-root", str(folder / "output")]) + self.assertNotIn("No DXBC:", log, "Decompiler silently dropped a blob record") + shaders = list((folder / "output").glob("*.shader")) + self.assertEqual(1, len(shaders)) + text = shaders[0].read_text(encoding="utf-8-sig") + self.assertIn('Shader "' + data["m_ParsedForm"]["m_Name"] + '"', text) + self.assertIn("#pragma vertex", text) + self.assertIn("#pragma fragment", text) + + def run_checked(self, command): + result = subprocess.run(command, cwd=ROOT, capture_output=True, text=True, timeout=600) + self.assertEqual(0, result.returncode, result.stdout + result.stderr) + return result.stdout + result.stderr + + def test_bilateral_blur(self): + self.check_shader("BilateralBlur") + + def test_blit_add(self): + self.check_shader("BlitAdd") + + def test_volumetric_light(self): + self.check_shader("VolumetricLight") + + +if __name__ == "__main__": + unittest.main() From 25e1a4567bb37204127a7a0f0d12a64c1496f61c Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 16:15:10 +0100 Subject: [PATCH 57/93] test: verify referenced DirectX programs and document schema dependency --- README.md | 2 +- docs/volumetric-lights-pr.md | 23 ++++++--- tests/Fixtures/VolumetricLights/README.md | 24 ++++++++- tests/e2e/test_volumetric_lights.py | 63 +++++++++++++++++++++-- 4 files changed, 99 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 7919e2f..d460cd4 100644 --- a/README.md +++ b/README.md @@ -411,7 +411,7 @@ If you change the pretty printer or surface-shader recognition, keep ## VolumetricLights regression corpus -The [VolumetricLights fixtures](tests/Fixtures/VolumetricLights/README.md) add three BSD-licensed shaders compiled with Unity 6000.0.75f1 / D3D11. Run `python tests/e2e/test_volumetric_lights.py` after building the Release CLI. This draft corpus currently exposes upstream JSON-schema and Unity 6 blob-reading failures; it does not claim rendering equivalence. +The [VolumetricLights fixtures](tests/Fixtures/VolumetricLights/README.md) add three BSD-licensed shaders compiled with Unity 6000.0.75f1 / D3D11. Run `python tests/e2e/test_volumetric_lights.py` after building the Release CLI. It checks all 199 expected DirectX programs and requires [PR #6](https://github.com/Abby1591/Unity_Shader_Decompiler/pull/6): the three shader cases fail before that schema fix and pass afterward. It does not claim generated-HLSL compilation or rendering equivalence. ## License diff --git a/docs/volumetric-lights-pr.md b/docs/volumetric-lights-pr.md index 192f4b3..63d25b5 100644 --- a/docs/volumetric-lights-pr.md +++ b/docs/volumetric-lights-pr.md @@ -1,10 +1,21 @@ ## Improvement -Add three BSD-licensed VolumetricLights shaders as an offline end-to-end regression corpus, with pinned source provenance, Unity 6000.0.75f1 D3D11 exports, migration snapshots, and integrity hashes. No production behavior changes. +Add three BSD-licensed VolumetricLights shaders as an offline regression corpus, with pinned source provenance, Unity 6000.0.75f1 D3D11 exports, migration snapshots, and integrity hashes. No production behavior changes. -## Validation / blockers +## Dependency / merge order -- All three input shaders compile and the asset bundle builds with Unity CLI 6000.0.75f1 / D3D11. -- Fixture integrity passes; all three extraction tests currently fail on upstream `417f26b` (`m_CompressedBlob` schema mismatch). Requires the schema support from #6. -- A diagnostic extraction with schema support reveals additional swallowed Unity 6 blob-reading errors in upstream. Keep this PR in draft; do not relax the assertions to accept partial output. -- These tests do not prove rendered equivalence or runtime keyword selection. +**Depends on #6. Merge #6 before #14.** No other correction PR is required for the current assertions. Keep this PR in draft until its base includes #6. + +## Validation + +The same seven tests were run against: + +- Upstream `417f26b`: three shader cases fail at extraction (`m_CompressedBlob`); four integrity/guard tests pass. +- #6 alone (`9677441`): **7/7 pass**. +- All twelve correction PRs integrated with upstream `417f26b`: **7/7 pass**. + +The tests require every serialized DirectX code reference to produce a function of the expected stage and a DXBC artifact: **13 BilateralBlur + 2 BlitAdd + 184 VolumetricLight = 199 programs**. They also check final shader identity and vertex/fragment pragmas. Negative tests verify missing programs cannot pass. + +The previous blanket `No DXBC:` assertion was incorrect: these D3D11-only exports also contain parameter records, whose indices are separate from code references. Reading those records as program headers can produce misleading GLES labels. The corrected assertions use the original JSON code references, not those labels or a blanket log filter. + +All three input shaders compile with Unity CLI 6000.0.75f1 / D3D11. These tests do not verify compilation of generated HLSL, runtime keyword selection, or rendered equivalence, and do not demonstrate a dependency on the other correction PRs. See [fixture documentation](tests/Fixtures/VolumetricLights/README.md) for the `USD_REPO_ROOT` before/after procedure. diff --git a/tests/Fixtures/VolumetricLights/README.md b/tests/Fixtures/VolumetricLights/README.md index 58e71c3..71c1950 100644 --- a/tests/Fixtures/VolumetricLights/README.md +++ b/tests/Fixtures/VolumetricLights/README.md @@ -27,6 +27,26 @@ dotnet build src/UnityShaderDecompiler.Cli/UnityShaderDecompiler.Cli.csproj -c R python tests/e2e/test_volumetric_lights.py ``` -The test checks fixture integrity, D3D11-only inputs, extraction, CLI completion, absence of swallowed blob errors, and output shader identity/stages. It does **not** establish GPU rendering equivalence, complete runtime keyword selection, or successful recompilation of the generated HLSL. +The tests read expected vertex/fragment code indices directly from the original JSON, require a collected function with the matching stage for every index, and verify each saved DXBC artifact. Parameter-record indices are not code references: interpreting them as program headers can print misleading GLES labels and `No DXBC:` diagnostics. Such diagnostics alone do not establish a lost DirectX program. A missing expected program always fails. -On upstream `417f26b`, all three extraction cases fail because the JSON contains `compressedBlob`, while the extractor requires `m_CompressedBlob`. PR #6 addresses schema compatibility. A diagnostic run using the compatible extractor also exposes `No DXBC: Unknown shader blob format` on upstream's Unity 6 blob reader. These failures are intentional regression signals, not skipped tests. Keep this PR in draft until the production fixes are available and the full cases pass. +Coverage: BilateralBlur **13** programs (5 vertex / 8 fragment), BlitAdd **2** (1 / 1), and VolumetricLight **184** (76 / 108): **199 unique programs across the three fixtures**. These checks establish extraction and per-program decompilation coverage, not successful recompilation of generated HLSL, complete runtime keyword selection, or GPU rendering equivalence. + +## Dependency and before/after validation + +**Merge [PR #6](https://github.com/Abby1591/Unity_Shader_Decompiler/pull/6) before [PR #14](https://github.com/Abby1591/Unity_Shader_Decompiler/pull/14).** No other PR is required for these assertions. + +| Implementation tested with the same corpus/tests | Result | +| --- | --- | +| Upstream `417f26b` | Three shader tests fail at extraction (`m_CompressedBlob`); four integrity/guard tests pass | +| PR #6 alone, `9677441` | All seven tests pass; all 199 expected DirectX programs are collected | +| All twelve correction PRs integrated with upstream `417f26b` | All seven tests pass; all 199 expected DirectX programs are collected | + +To repeat the comparison without copying or editing fixtures, build the Release CLI in each target checkout, then run this test file with `USD_REPO_ROOT` set to that checkout. For example, in PowerShell from this PR's repository root: + +```powershell +$env:USD_REPO_ROOT = (Resolve-Path ../decompiler-under-test).Path +python tests/e2e/test_volumetric_lights.py +Remove-Item Env:USD_REPO_ROOT +``` + +Keep #14 in draft until #6 is integrated into its base. Passing this corpus does not independently prove the necessity or correctness of the other correction PRs. diff --git a/tests/e2e/test_volumetric_lights.py b/tests/e2e/test_volumetric_lights.py index 34cf3fd..9b07e4e 100644 --- a/tests/e2e/test_volumetric_lights.py +++ b/tests/e2e/test_volumetric_lights.py @@ -1,17 +1,50 @@ """Offline regression corpus; build the Release CLI before running this file.""" import hashlib import json +import os +import re import subprocess import sys import tempfile import unittest from pathlib import Path -ROOT = Path(__file__).resolve().parents[2] -FIXTURES = ROOT / "tests/Fixtures/VolumetricLights" +CORPUS_ROOT = Path(__file__).resolve().parents[2] +ROOT = Path(os.environ.get("USD_REPO_ROOT", CORPUS_ROOT)).resolve() +FIXTURES = CORPUS_ROOT / "tests/Fixtures/VolumetricLights" CLI = ROOT / "src/UnityShaderDecompiler.Cli/bin/Release/net10.0/Shader Decompiler.dll" +def expected_programs(data): + """Read code references, never parameter-record indices, from the export.""" + stages = {"progVertex": ({15, 16}, "Vertex"), + "progFragment": ({17, 18}, "Fragment")} + expected = {} + for subshader in data["m_ParsedForm"]["m_SubShaders"]: + for shader_pass in subshader["m_Passes"]: + for field, (types, stage) in stages.items(): + program = shader_pass[field] + groups = [program["m_SubPrograms"], *program["m_PlayerSubPrograms"]] + references = [item for group in groups for item in group + if item["m_GpuProgramType"] in types] + if not references: + raise AssertionError(f"No DirectX {stage} references in a fixture pass") + for item in references: + index = item["m_BlobIndex"] + if index in expected and expected[index] != stage: + raise AssertionError(f"Conflicting stages for program {index}") + expected[index] = stage + return expected + + +def verify_programs(expected, log): + collected = {int(index): stage for stage, index in re.findall( + r"Stage 2: collected (\w+) function '[^']+' \(subprogram (\d+)\)\.", log)} + for index, stage in expected.items(): + if collected.get(index) != stage: + raise AssertionError(f"Missing DirectX {stage} program {index}") + + class VolumetricLightsTests(unittest.TestCase): def test_fixture_integrity(self): manifest = json.loads((FIXTURES / "manifest.json").read_text()) @@ -23,14 +56,20 @@ def check_shader(self, name): source = FIXTURES / "Json" / (name + ".json") data = json.loads(source.read_text(encoding="utf-8-sig")) self.assertEqual([4], data["platforms"], "Fixture must contain only D3D11") + expected = expected_programs(data) self.assertTrue(CLI.is_file(), "Build the Release CLI first") with tempfile.TemporaryDirectory(prefix="volumetric-lights-") as temporary: folder = Path(temporary) self.run_checked([sys.executable, str(ROOT / "Extract.py"), str(source), "--flat", "--out-dir", str(folder / "input")]) log = self.run_checked(["dotnet", str(CLI), str(folder / "input"), - "--no-fuse-temps", "--out-root", str(folder / "output")]) - self.assertNotIn("No DXBC:", log, "Decompiler silently dropped a blob record") + "--no-fuse-temps", "--save-subprograms", + "--out-root", str(folder / "output")]) + verify_programs(expected, log) + for index in expected: + dxbc = folder / "output" / f"program{index}.dxbc" + self.assertTrue(dxbc.is_file(), f"Missing DXBC artifact for program {index}") + self.assertEqual(b"DXBC", dxbc.read_bytes()[:4]) shaders = list((folder / "output").glob("*.shader")) self.assertEqual(1, len(shaders)) text = shaders[0].read_text(encoding="utf-8-sig") @@ -52,6 +91,22 @@ def test_blit_add(self): def test_volumetric_light(self): self.check_shader("VolumetricLight") + def test_parameter_records_are_not_code_references(self): + data = json.loads((FIXTURES / "Json/BlitAdd.json").read_text()) + self.assertEqual({1: "Vertex", 3: "Fragment"}, expected_programs(data)) + + def test_missing_directx_program_is_rejected(self): + log = "Stage 2: collected Vertex function 'vert' (subprogram 1)." + with self.assertRaisesRegex(AssertionError, "Missing DirectX Fragment program 3"): + verify_programs({1: "Vertex", 3: "Fragment"}, log) + + def test_parameter_diagnostic_does_not_hide_program_loss(self): + log = "No DXBC: Unknown shader blob format.\n" + with self.assertRaisesRegex(AssertionError, "Missing DirectX Vertex program 1"): + verify_programs({1: "Vertex"}, log) + log += "Stage 2: collected Vertex function 'vert' (subprogram 1)." + verify_programs({1: "Vertex"}, log) + if __name__ == "__main__": unittest.main() From 851ce60715d33570f34018fdde39678579ad18a1 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 16:21:11 +0100 Subject: [PATCH 58/93] Keep pull request descriptions on GitHub only --- docs/pr-integer-shader-properties.md | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 docs/pr-integer-shader-properties.md diff --git a/docs/pr-integer-shader-properties.md b/docs/pr-integer-shader-properties.md deleted file mode 100644 index 97b25d3..0000000 --- a/docs/pr-integer-shader-properties.md +++ /dev/null @@ -1,7 +0,0 @@ -# Preserve serialized integer shader properties - -Property type 5 stores an integer payload in a serialized float slot. Emit ShaderLab `Integer` and recover the original bits instead of treating that slot as a numeric float. Missing or non-finite JSON payloads fail explicitly because they cannot recover the original value reliably. - -Validation: 59 C# tests and all four PicaVoxel end-to-end cases pass, including positive values, negative zero bits, large integers and invalid payloads. The isolated suite uses en-US because upstream's fr-FR golden-output failure is addressed separately in #5. No game-specific fixtures or paths are included. - -Upstream sync: merged `417f26b` (Core/Cli split and namespace rename). Adapted regression tests to the new layout and shared DXBC compiler; no test assertions were relaxed. From 49b8bab6e8eaed23cde0c998b509cab4ea4f6f47 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 16:21:11 +0100 Subject: [PATCH 59/93] Keep pull request descriptions on GitHub only --- docs/volumetric-lights-pr.md | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 docs/volumetric-lights-pr.md diff --git a/docs/volumetric-lights-pr.md b/docs/volumetric-lights-pr.md deleted file mode 100644 index 63d25b5..0000000 --- a/docs/volumetric-lights-pr.md +++ /dev/null @@ -1,21 +0,0 @@ -## Improvement - -Add three BSD-licensed VolumetricLights shaders as an offline regression corpus, with pinned source provenance, Unity 6000.0.75f1 D3D11 exports, migration snapshots, and integrity hashes. No production behavior changes. - -## Dependency / merge order - -**Depends on #6. Merge #6 before #14.** No other correction PR is required for the current assertions. Keep this PR in draft until its base includes #6. - -## Validation - -The same seven tests were run against: - -- Upstream `417f26b`: three shader cases fail at extraction (`m_CompressedBlob`); four integrity/guard tests pass. -- #6 alone (`9677441`): **7/7 pass**. -- All twelve correction PRs integrated with upstream `417f26b`: **7/7 pass**. - -The tests require every serialized DirectX code reference to produce a function of the expected stage and a DXBC artifact: **13 BilateralBlur + 2 BlitAdd + 184 VolumetricLight = 199 programs**. They also check final shader identity and vertex/fragment pragmas. Negative tests verify missing programs cannot pass. - -The previous blanket `No DXBC:` assertion was incorrect: these D3D11-only exports also contain parameter records, whose indices are separate from code references. Reading those records as program headers can produce misleading GLES labels. The corrected assertions use the original JSON code references, not those labels or a blanket log filter. - -All three input shaders compile with Unity CLI 6000.0.75f1 / D3D11. These tests do not verify compilation of generated HLSL, runtime keyword selection, or rendered equivalence, and do not demonstrate a dependency on the other correction PRs. See [fixture documentation](tests/Fixtures/VolumetricLights/README.md) for the `USD_REPO_ROOT` before/after procedure. From e96b31facab4cba1e3b4e1aab81e752e450365a5 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 16:21:11 +0100 Subject: [PATCH 60/93] Keep pull request descriptions on GitHub only --- docs/pr-structured-buffer-loads.md | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 docs/pr-structured-buffer-loads.md diff --git a/docs/pr-structured-buffer-loads.md b/docs/pr-structured-buffer-loads.md deleted file mode 100644 index 8537964..0000000 --- a/docs/pr-structured-buffer-loads.md +++ /dev/null @@ -1,15 +0,0 @@ -# Preserve register bits and structured-buffer loads - -Float temporary storage can corrupt integer moves and structured-buffer results. With `--no-fuse-temps`, preserve raw uint storage, typed reads and outputs, full-bit comparison masks and integer source modifiers. Keep arithmetic rewrites from removing the addition before fractional-part evaluation. - -Structured loads preserve element stride, byte offset and resource swizzle independently. These changes belong together because correct addressing alone still loses integer output bits without raw register transport. Default options now select raw storage and skip unsafe fusion for functions containing structured loads; `--no-fuse-temps` remains supported. Constant and dynamic byte offsets work, including bit-field address calculations emitted by fxc. Invalid constant offsets fail explicitly. This PR includes the former #7; no separate dependency remains. - -Compatibility: retain the parameterless `IRBuilder` constructor, the original two-parameter `IRPipeline.Run` overload and existing texture-operation enum values. New overloads and enum members are additive. - -Saturated moves clamp the floating-point value before storing its bits. Integer reads bypass float round trips that fxc can fold to zero for known subnormals; this preserves low-bit branch conditions. - -Native round trips cover signed/unsigned moves, saturation and low-bit selection, plus float/uint/int structured loads with stride, offset and swizzle. A restricted bytecode evaluator checks exact output words, including NaN payloads, subnormals and signed zero, and rejects unexpected operations. The saturation regression fails with the former implementation and passes with the fix. These are compiler/bytecode checks, not GPU execution tests. - -Validation: 72 C# tests and all four PicaVoxel cases pass on Windows. New regressions fail before the compatibility fix and pass afterward, covering default options, dynamic offsets and the old API. Isolated C# tests use en-US; the upstream fr-FR golden-output failure is addressed separately in #5. - -Upstream sync: merged `417f26b` (Core/Cli split and namespace rename). Adapted regression tests to the new layout and shared DXBC compiler; no test assertions were relaxed. From 1051dc2a16130507f65d810856d86c7b04b4df7f Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 16:21:11 +0100 Subject: [PATCH 61/93] Keep pull request descriptions on GitHub only --- docs/pr-temporary-register-names.md | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 docs/pr-temporary-register-names.md diff --git a/docs/pr-temporary-register-names.md b/docs/pr-temporary-register-names.md deleted file mode 100644 index d934c1a..0000000 --- a/docs/pr-temporary-register-names.md +++ /dev/null @@ -1,7 +0,0 @@ -# Keep temporary register identities distinct - -Name recovery could give distinct temporary registers the same HLSL identifier. Preserve register identity when choosing temporary names to prevent collisions. Regenerate the shared golden output after combining emitter changes. - -Validation: 52 C# tests and all four existing PicaVoxel extraction/decompilation tests pass. C# tests use an en-US default culture; the upstream fr-FR golden-output failure is addressed independently by the numeric-formatting PR. The golden output was regenerated by the emitter for this change only; existing comparison assertions are unchanged. - -Upstream sync: merged `417f26b` (Core/Cli split and namespace rename). Adapted regression tests to the new layout and shared DXBC compiler; no test assertions were relaxed. From f152b1eaf39a9101592cedd642ce7323012c9561 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 16:21:11 +0100 Subject: [PATCH 62/93] Keep pull request descriptions on GitHub only --- docs/pr-vector-expression-widths.md | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 docs/pr-vector-expression-widths.md diff --git a/docs/pr-vector-expression-widths.md b/docs/pr-vector-expression-widths.md deleted file mode 100644 index 182bc24..0000000 --- a/docs/pr-vector-expression-widths.md +++ /dev/null @@ -1,7 +0,0 @@ -# Preserve vector lanes in casts and dot products - -Vector casts could collapse to one lane, and dot-product operands could retain the wrong width. Preserve the expression width so the emitted HLSL consumes the intended components. - -Validation: 54 C# tests and all four existing PicaVoxel extraction/decompilation tests pass. C# tests use an en-US default culture; the upstream fr-FR golden-output failure is addressed independently by the numeric-formatting PR. The golden output was regenerated by the emitter for this change only; existing comparison assertions are unchanged. - -Upstream sync: merged `417f26b` (Core/Cli split and namespace rename). Adapted regression tests to the new layout and shared DXBC compiler; no test assertions were relaxed. From 72b99af68a66f9a41b1c49d74f42ed238de7d7f6 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 16:21:11 +0100 Subject: [PATCH 63/93] Keep pull request descriptions on GitHub only --- docs/pr-serialized-variant-pairing.md | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 docs/pr-serialized-variant-pairing.md diff --git a/docs/pr-serialized-variant-pairing.md b/docs/pr-serialized-variant-pairing.md deleted file mode 100644 index bb3478a..0000000 --- a/docs/pr-serialized-variant-pairing.md +++ /dev/null @@ -1,14 +0,0 @@ -# Restore runtime keyword selection from serialized variants - -- **Bug fix:** Match serialized stage references and keyword states, retain all graphics stages in expanded mode, and preserve constant-buffer sizes. -- **Improvement:** Add opt-in runtime keyword selection with local/global scope and an exhaustive compilation manifest. - -Use serialized program references and stage keyword masks to pair vertex/fragment programs. Preserve stage-only keywords and reject missing programs, unmatched states and incompatible interfaces. Keep constant-buffer sizes in extracted metadata. - -Compatibility: default expanded passes also retain Geometry, Hull and Domain programs, matching their shared keyword states across the complete graphics pipeline. Default expanded mode also preserves log-and-continue after a recorded subprogram error; runtime reconstruction remains strict. Only the opt-in runtime reconstruction is restricted to vertex/fragment stages. A native geometry fixture reproduces the previous default-mode rejection and now passes; unit tests cover tessellation/geometry combinations and unmatched intermediate stages. - -`--restore-keyword-variants` retains the original passes and emits keyword-controlled program branches, including local/global scope. Reconstruct only exact, complete combinations of independent or exclusive keyword sets; missing scope flags, ambiguous mappings and unsupported correlations fail explicitly. The default expanded-pass mode remains available. A hash-bound manifest lets native compilation check every recovered combination. - -Validation: 78 C# tests, six Python tests and all four PicaVoxel cases pass in isolation. The prior runtime validation under Unity CLI with Editor 6000.0.75f1 and D3D11 passed nine GPU readbacks; the compatibility changes leave that opt-in path unchanged. These tests do not establish full application rendering or resource-binding equivalence. Isolated C# tests use en-US; the existing fr-FR golden issue is addressed separately in #5. - -Upstream sync: merged `417f26b` (Core/Cli split and namespace rename). Adapted regression tests to the new layout and shared DXBC compiler; no test assertions were relaxed. From 8f960d56ad6a72290d4e3fc42a66a6ba662d96e5 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 16:21:12 +0100 Subject: [PATCH 64/93] Keep pull request descriptions on GitHub only --- docs/pr-resource-binding-layouts.md | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 docs/pr-resource-binding-layouts.md diff --git a/docs/pr-resource-binding-layouts.md b/docs/pr-resource-binding-layouts.md deleted file mode 100644 index 36cf0de..0000000 --- a/docs/pr-resource-binding-layouts.md +++ /dev/null @@ -1,7 +0,0 @@ -# Preserve resource bindings and raw constant-buffer layout - -Resources with the same recovered name could collapse despite occupying different slots. Keep bindings distinct. For partially known constant buffers, emit raw storage from byte zero while preserving the serialized extent, name and binding; native reflection tests verify that layout. The variant-metadata PR must retain the `size` field consumed here. - -Validation: 57 C# tests and all four existing PicaVoxel extraction/decompilation tests pass. C# tests use an en-US default culture; the upstream fr-FR golden-output failure is addressed independently by the numeric-formatting PR. - -Upstream sync: merged `417f26b` (Core/Cli split and namespace rename). Adapted regression tests to the new layout and shared DXBC compiler; no test assertions were relaxed. From 143323e2a599ff529ba68fc659b5b322aaf4bdfd Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 16:21:12 +0100 Subject: [PATCH 65/93] Keep pull request descriptions on GitHub only --- docs/pr-invariant-numeric-output.md | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 docs/pr-invariant-numeric-output.md diff --git a/docs/pr-invariant-numeric-output.md b/docs/pr-invariant-numeric-output.md deleted file mode 100644 index db807d1..0000000 --- a/docs/pr-invariant-numeric-output.md +++ /dev/null @@ -1,7 +0,0 @@ -# Use invariant numeric literals in shader output - -Shader output used the current locale, producing invalid literals such as `0,5` under fr-FR. Emit `0.5` consistently and retain round-trip precision for floats. Locale tests cover French and German formatting. - -Validation: 53 C# tests and all four existing PicaVoxel extraction/decompilation tests pass. - -Upstream sync: merged `417f26b` (Core/Cli split and namespace rename). Adapted regression tests to the new layout and shared DXBC compiler; no test assertions were relaxed. From 54274f009bf961c405f25fc86099fab9290088e4 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 16:21:12 +0100 Subject: [PATCH 66/93] Keep pull request descriptions on GitHub only --- docs/pr-explicit-texture-lod.md | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 docs/pr-explicit-texture-lod.md diff --git a/docs/pr-explicit-texture-lod.md b/docs/pr-explicit-texture-lod.md deleted file mode 100644 index 475f8b2..0000000 --- a/docs/pr-explicit-texture-lod.md +++ /dev/null @@ -1,7 +0,0 @@ -# Keep explicit zero texture LOD explicit - -An explicit texture LOD of zero was emitted as implicit sampling, changing mip selection. Preserve `SampleLevel(..., 0)` when the source instruction specifies an explicit LOD. Keep the public `IRTexturePatternRecognition.Run` entry point as a no-op so existing callers still compile and link. - -Validation: 52 C# tests and all four existing PicaVoxel extraction/decompilation tests pass. C# tests use an en-US default culture; the upstream fr-FR golden-output failure is addressed independently by the numeric-formatting PR. The golden output was regenerated by the emitter for this change only; existing comparison assertions are unchanged. - -Upstream sync: merged `417f26b` (Core/Cli split and namespace rename). Adapted regression tests to the new layout and shared DXBC compiler; no test assertions were relaxed. From c56230875a6b9c842cf0d1168a5ab4b00d7bcd33 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 16:21:12 +0100 Subject: [PATCH 67/93] Keep pull request descriptions on GitHub only --- docs/pr-metadata-offsets.md | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 docs/pr-metadata-offsets.md diff --git a/docs/pr-metadata-offsets.md b/docs/pr-metadata-offsets.md deleted file mode 100644 index 5c7c689..0000000 --- a/docs/pr-metadata-offsets.md +++ /dev/null @@ -1,9 +0,0 @@ -# Accept AssetStudio and AssetRipper shader JSON schemas - -Extraction fails when an export uses legacy `m_Index` offsets or different field/map encodings. Normalize both exporters at the input boundary, prefer `m_OffsetInConstantBuffer` when present, and fall back to `m_Index`. Reject missing/invalid offsets, conflicting aliases and duplicate map keys. Source JSON files remain unchanged. - -Reproducible validation: ten generic Python tests, 51 baseline C# tests and all four checked-in PicaVoxel extraction/decompilation cases pass. The isolated C# suite uses en-US; upstream's fr-FR golden-output failure is addressed separately in #5. No existing assertions or generated references are changed. - -Additional local validation used two external shaders exported from the same bundle with AssetStudioMod CLI v0.20.0 and AssetRipper. Their normalized metadata and decompressed blobs match. With the separate emitter fixes combined, all four exports also produced identical artifacts across exporters and passed native recompilation and finite interpreter comparisons with serialized constants. Those external fixtures are not distributed, so this additional check is not reproducible from the PR alone and does not prove GPU equivalence. - -Upstream sync: merged `417f26b` (Core/Cli split and namespace rename). Adapted regression tests to the new layout and shared DXBC compiler; no test assertions were relaxed. From 641e78774a799af18ff8c9bd419ba52ac3957f20 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 16:21:12 +0100 Subject: [PATCH 68/93] Keep pull request descriptions on GitHub only --- docs/pr-directx-fail-fast.md | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 docs/pr-directx-fail-fast.md diff --git a/docs/pr-directx-fail-fast.md b/docs/pr-directx-fail-fast.md deleted file mode 100644 index 8e952de..0000000 --- a/docs/pr-directx-fail-fast.md +++ /dev/null @@ -1,7 +0,0 @@ -# Propagate DirectX subprogram decompilation failures - -A corrupt DirectX subprogram is logged and omitted by default. Opt in with `--fail-on-error` (API: `DecompileOptions.FailOnError = true`) to abort with the subprogram index and original exception, before writing the final shader. Existing commands and API calls retain their default behavior. - -Validation: 55 C# tests cover both failure policies; all four existing PicaVoxel extraction/decompilation tests pass. Isolated C# tests use en-US; the upstream fr-FR golden-output failure is addressed separately in #5. - -Upstream sync: merged `417f26b` (Core/Cli split and namespace rename). Adapted regression tests to the new layout and shared DXBC compiler; no test assertions were relaxed. From 055418bec5ae9ff57927c51dd93ae4a80e415709 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 16:21:12 +0100 Subject: [PATCH 69/93] Keep pull request descriptions on GitHub only --- docs/pr-conditional-value-scope.md | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 docs/pr-conditional-value-scope.md diff --git a/docs/pr-conditional-value-scope.md b/docs/pr-conditional-value-scope.md deleted file mode 100644 index d5eeaf7..0000000 --- a/docs/pr-conditional-value-scope.md +++ /dev/null @@ -1,7 +0,0 @@ -# Preserve values across conditional control flow - -Values assigned inside a conditional could be referenced outside their declaration scope, and phi copies could run before a nested join. Declare branch-written values in the enclosing scope and place copies after the closing conditional. Tests cover reads after an if and copies after a nested join; they do not exhaust every loop/switch combination. - -Validation: 53 C# tests and all four existing PicaVoxel extraction/decompilation tests pass. C# tests use an en-US default culture; the upstream fr-FR golden-output failure is addressed independently by the numeric-formatting PR. The golden output was regenerated by the emitter for this change only; existing comparison assertions are unchanged. - -Upstream sync: merged `417f26b` (Core/Cli split and namespace rename). Adapted regression tests to the new layout and shared DXBC compiler; no test assertions were relaxed. From a7179d89dae82be144d6496dcb5de8ab25fa1baa Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 16:21:12 +0100 Subject: [PATCH 70/93] Keep pull request descriptions on GitHub only --- docs/pr-dxbc-feature-flags.md | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 docs/pr-dxbc-feature-flags.md diff --git a/docs/pr-dxbc-feature-flags.md b/docs/pr-dxbc-feature-flags.md deleted file mode 100644 index 31d74fe..0000000 --- a/docs/pr-dxbc-feature-flags.md +++ /dev/null @@ -1,7 +0,0 @@ -# Read SFI0 as a 64-bit feature mask - -`SFI0` is an eight-byte feature mask, not a statistics structure. Read its 64 bits and reject invalid chunk lengths. Legacy public uint fields remain for source/binary compatibility, but are not populated by SFI0: `HasLegacyStatistics` explicitly returns false. Their default values must not be interpreted as recovered statistics; consumers should use `FeatureFlags` for this chunk. - -Validation: 54 C# tests, including the native feature mask and the 35 retained public fields, and all four PicaVoxel cases pass. Isolated C# tests use en-US; the upstream fr-FR golden-output failure is addressed separately in #5. - -Upstream sync: merged `417f26b` (Core/Cli split and namespace rename). Adapted regression tests to the new layout and shared DXBC compiler; no test assertions were relaxed. From 8d65d16f47c9d92b58ab265d3fa8cf992ea5bcee Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 16:50:11 +0100 Subject: [PATCH 71/93] Refactor new code to follow project conventions --- .../Core/Hlsl/HlslPrettyPrinter.cs | 2 ++ .../DXBC/Metadata/Shadermetadata.cs | 2 ++ .../IntegerPropertyTests.cs | 11 +++++++++-- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslPrettyPrinter.cs b/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslPrettyPrinter.cs index 8ba661b..dd3b5b8 100644 --- a/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslPrettyPrinter.cs +++ b/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslPrettyPrinter.cs @@ -116,7 +116,9 @@ private static string PropertyDefaultText(HlslPropertyNode p) : p.DefaultValue is { } integerDefault && integerDefault.TryGetProperty("x", out JsonElement x) ? x.GetSingle() : throw new InvalidDataException("Integer property has no serialized default."); if (!float.IsFinite(value)) + { throw new InvalidDataException("Integer property bits cannot be recovered from a non-finite JSON float."); + } return BitConverter.SingleToInt32Bits(value).ToString(System.Globalization.CultureInfo.InvariantCulture); } diff --git a/src/UnityShaderDecompiler.Core/DXBC/Metadata/Shadermetadata.cs b/src/UnityShaderDecompiler.Core/DXBC/Metadata/Shadermetadata.cs index c499ea2..98a09d4 100644 --- a/src/UnityShaderDecompiler.Core/DXBC/Metadata/Shadermetadata.cs +++ b/src/UnityShaderDecompiler.Core/DXBC/Metadata/Shadermetadata.cs @@ -126,7 +126,9 @@ private static float[] ParseDefaultComponents(JsonElement el) || integerDefault.ValueKind != JsonValueKind.Object || !integerDefault.TryGetProperty("x", out JsonElement integerBits) || integerBits.ValueKind != JsonValueKind.Number)) + { throw new InvalidDataException("Integer property requires a numeric serialized default."); + } if (!el.TryGetProperty("defaultValue", out var dv) || dv.ValueKind != JsonValueKind.Object) return Array.Empty(); diff --git a/tests/UnityShaderDecompiler.Tests/IntegerPropertyTests.cs b/tests/UnityShaderDecompiler.Tests/IntegerPropertyTests.cs index 21660fb..d2f4e1b 100644 --- a/tests/UnityShaderDecompiler.Tests/IntegerPropertyTests.cs +++ b/tests/UnityShaderDecompiler.Tests/IntegerPropertyTests.cs @@ -4,8 +4,10 @@ namespace UnityShaderDecompiler.Tests; +// Covers integer property tests. public sealed class IntegerPropertyTests { + // Verifies that serialized integer defaults preserve bits. [Theory] [InlineData(0)] [InlineData(1)] @@ -21,24 +23,29 @@ public void SerializedIntegerDefaultsPreserveBits(int expected) }; using JsonDocument json = JsonDocument.Parse(JsonSerializer.Serialize(new { - name = "_Count", type = 5, defaultValue = new { x = serialized, y = 0, z = 0, w = 0 }, + name = "_Count", + type = 5, + defaultValue = new { x = serialized, y = 0, z = 0, w = 0 }, })); metadata.Properties.Add(ShaderProperty.From(json.RootElement)); string text = HlslPrettyPrinter.Print(HlslAstBuilder.BuildShell(metadata)); Assert.Contains($"_Count (\"_Count\", Integer) = {expected}", text); } + // Verifies that non finite default cannot recover an integer payload. [Fact] public void NonFiniteDefaultCannotRecoverAnIntegerPayload() { HlslShaderNode shader = new HlslShaderNode(); shader.Properties.Add(new HlslPropertyNode { - Kind = HlslPropertyKind.Integer, DefaultComponents = [float.NaN, 0, 0, 0], + Kind = HlslPropertyKind.Integer, + DefaultComponents = [float.NaN, 0, 0, 0], }); Assert.Throws(() => HlslPrettyPrinter.Print(shader)); } + // Verifies that missing or unrecoverable integer defaults fail. [Theory] [InlineData("{\"type\":5}")] [InlineData("{\"type\":5,\"defaultValue\":{\"x\":\"NaN\"}}")] From cbd50bd2b45cf6140ecae3d03cf4f4acdce7e676 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 16:50:11 +0100 Subject: [PATCH 72/93] Refactor new code to follow project conventions --- .../VolumetricLights/BuildFixtures.cs | 48 ++++++++++++++----- tests/Fixtures/VolumetricLights/README.md | 8 ++-- tests/e2e/test_volumetric_lights.py | 13 +++++ 3 files changed, 51 insertions(+), 18 deletions(-) diff --git a/tests/Fixtures/VolumetricLights/BuildFixtures.cs b/tests/Fixtures/VolumetricLights/BuildFixtures.cs index 7cf5fe8..139b798 100644 --- a/tests/Fixtures/VolumetricLights/BuildFixtures.cs +++ b/tests/Fixtures/VolumetricLights/BuildFixtures.cs @@ -5,19 +5,50 @@ using UnityEngine; using UnityEngine.Rendering; -// Copy into Assets/Editor in a disposable Built-in project. +// Builds the D3D11 fixture bundle and rejects shader import errors. public static class BuildFixtures { + // Runs the validation and writes its result report. public static void Run() { File.WriteAllText("fixture-build.json", "{\"passed\":false}"); if (Application.unityVersion != "6000.0.75f1") + { throw new InvalidOperationException("Fixture generation requires Unity 6000.0.75f1."); + } if (SystemInfo.graphicsDeviceType != GraphicsDeviceType.Direct3D11) + { throw new InvalidOperationException("Fixture generation requires Direct3D 11."); + } string[] assets = Directory.GetFiles("Assets/VolumetricLights", "*.shader") .Select(path => path.Replace('\\', '/')).OrderBy(path => path, StringComparer.Ordinal).ToArray(); - if (assets.Length != 3) throw new InvalidOperationException("Expected the three upstream shaders."); + if (assets.Length != 3) + { + throw new InvalidOperationException("Expected the three upstream shaders."); + } + ConfigureShaders(assets); + Directory.CreateDirectory("FixtureBundle"); + AssetBundleBuild bundle = new AssetBundleBuild { assetBundleName = "volumetriclights", assetNames = assets }; + AssetBundleManifest manifest = BuildPipeline.BuildAssetBundles("FixtureBundle", new[] { bundle }, + BuildAssetBundleOptions.ForceRebuildAssetBundle | BuildAssetBundleOptions.StrictMode, + BuildTarget.StandaloneWindows64); + if (manifest == null || !File.Exists("FixtureBundle/volumetriclights")) + { + throw new InvalidOperationException("Fixture bundle compilation failed."); + } + foreach (string path in assets) + { + if (ShaderUtil.ShaderHasError(AssetDatabase.LoadAssetAtPath(path))) + { + throw new InvalidOperationException("Shader compilation failed: " + path); + } + } + File.WriteAllText("fixture-build.json", "{\"passed\":true,\"editor\":\"6000.0.75f1\",\"backend\":\"Direct3D11\",\"shaders\":3}"); + } + + // Configures D3D11 and includes every fixture shader in the bundle. + private static void ConfigureShaders(string[] assets) + { PlayerSettings.SetUseDefaultGraphicsAPIs(BuildTarget.StandaloneWindows64, false); PlayerSettings.SetGraphicsAPIs(BuildTarget.StandaloneWindows64, new[] { GraphicsDeviceType.Direct3D11 }); SerializedObject graphics = new SerializedObject(AssetDatabase.LoadAllAssetsAtPath("ProjectSettings/GraphicsSettings.asset")[0]); @@ -28,21 +59,12 @@ public static void Run() AssetDatabase.ImportAsset(assets[index], ImportAssetOptions.ForceSynchronousImport); Shader shader = AssetDatabase.LoadAssetAtPath(assets[index]); if (shader == null || ShaderUtil.ShaderHasError(shader)) + { throw new InvalidOperationException("Shader import failed: " + assets[index]); + } included.GetArrayElementAtIndex(index).objectReferenceValue = shader; } graphics.ApplyModifiedPropertiesWithoutUndo(); AssetDatabase.SaveAssets(); - Directory.CreateDirectory("FixtureBundle"); - AssetBundleBuild bundle = new AssetBundleBuild { assetBundleName = "volumetriclights", assetNames = assets }; - AssetBundleManifest manifest = BuildPipeline.BuildAssetBundles("FixtureBundle", new[] { bundle }, - BuildAssetBundleOptions.ForceRebuildAssetBundle | BuildAssetBundleOptions.StrictMode, - BuildTarget.StandaloneWindows64); - if (manifest == null || !File.Exists("FixtureBundle/volumetriclights")) - throw new InvalidOperationException("Fixture bundle compilation failed."); - foreach (string path in assets) - if (ShaderUtil.ShaderHasError(AssetDatabase.LoadAssetAtPath(path))) - throw new InvalidOperationException("Shader compilation failed: " + path); - File.WriteAllText("fixture-build.json", "{\"passed\":true,\"editor\":\"6000.0.75f1\",\"backend\":\"Direct3D11\",\"shaders\":3}"); } } diff --git a/tests/Fixtures/VolumetricLights/README.md b/tests/Fixtures/VolumetricLights/README.md index 71c1950..cadbf8c 100644 --- a/tests/Fixtures/VolumetricLights/README.md +++ b/tests/Fixtures/VolumetricLights/README.md @@ -41,12 +41,10 @@ Coverage: BilateralBlur **13** programs (5 vertex / 8 fragment), BlitAdd **2** ( | PR #6 alone, `9677441` | All seven tests pass; all 199 expected DirectX programs are collected | | All twelve correction PRs integrated with upstream `417f26b` | All seven tests pass; all 199 expected DirectX programs are collected | -To repeat the comparison without copying or editing fixtures, build the Release CLI in each target checkout, then run this test file with `USD_REPO_ROOT` set to that checkout. For example, in PowerShell from this PR's repository root: +To repeat the comparison without copying or editing fixtures, build the Release CLI in each target checkout, then run this test file with `USD_REPO_ROOT` set to that checkout. For example, from this PR's repository root: -```powershell -$env:USD_REPO_ROOT = (Resolve-Path ../decompiler-under-test).Path -python tests/e2e/test_volumetric_lights.py -Remove-Item Env:USD_REPO_ROOT +```bash +USD_REPO_ROOT=../decompiler-under-test python tests/e2e/test_volumetric_lights.py ``` Keep #14 in draft until #6 is integrated into its base. Passing this corpus does not independently prove the necessity or correctness of the other correction PRs. diff --git a/tests/e2e/test_volumetric_lights.py b/tests/e2e/test_volumetric_lights.py index 9b07e4e..a2c6866 100644 --- a/tests/e2e/test_volumetric_lights.py +++ b/tests/e2e/test_volumetric_lights.py @@ -1,3 +1,4 @@ +# Checks shader extraction and decompilation behavior. """Offline regression corpus; build the Release CLI before running this file.""" import hashlib import json @@ -15,6 +16,7 @@ CLI = ROOT / "src/UnityShaderDecompiler.Cli/bin/Release/net10.0/Shader Decompiler.dll" +# Reads DirectX code indices separately from parameter-record indices. def expected_programs(data): """Read code references, never parameter-record indices, from the export.""" stages = {"progVertex": ({15, 16}, "Vertex"), @@ -37,6 +39,7 @@ def expected_programs(data): return expected +# Rejects any missing or incorrectly classified DirectX program. def verify_programs(expected, log): collected = {int(index): stage for stage, index in re.findall( r"Stage 2: collected (\w+) function '[^']+' \(subprogram (\d+)\)\.", log)} @@ -45,13 +48,16 @@ def verify_programs(expected, log): raise AssertionError(f"Missing DirectX {stage} program {index}") +# Covers shader metadata regression cases. class VolumetricLightsTests(unittest.TestCase): + # Checks fixture integrity. def test_fixture_integrity(self): manifest = json.loads((FIXTURES / "manifest.json").read_text()) for name, expected in manifest["files"].items(): with self.subTest(file=name): self.assertEqual(expected, hashlib.sha256((FIXTURES / name).read_bytes()).hexdigest()) + # Checks extraction and every expected DirectX program for one fixture. def check_shader(self, name): source = FIXTURES / "Json" / (name + ".json") data = json.loads(source.read_text(encoding="utf-8-sig")) @@ -77,29 +83,36 @@ def check_shader(self, name): self.assertIn("#pragma vertex", text) self.assertIn("#pragma fragment", text) + # Runs a pipeline command and rejects unsuccessful completion. def run_checked(self, command): result = subprocess.run(command, cwd=ROOT, capture_output=True, text=True, timeout=600) self.assertEqual(0, result.returncode, result.stdout + result.stderr) return result.stdout + result.stderr + # Checks bilateral blur. def test_bilateral_blur(self): self.check_shader("BilateralBlur") + # Checks blit add. def test_blit_add(self): self.check_shader("BlitAdd") + # Checks volumetric light. def test_volumetric_light(self): self.check_shader("VolumetricLight") + # Checks parameter records are not code references. def test_parameter_records_are_not_code_references(self): data = json.loads((FIXTURES / "Json/BlitAdd.json").read_text()) self.assertEqual({1: "Vertex", 3: "Fragment"}, expected_programs(data)) + # Checks missing directx program is rejected. def test_missing_directx_program_is_rejected(self): log = "Stage 2: collected Vertex function 'vert' (subprogram 1)." with self.assertRaisesRegex(AssertionError, "Missing DirectX Fragment program 3"): verify_programs({1: "Vertex", 3: "Fragment"}, log) + # Checks parameter diagnostic does not hide program loss. def test_parameter_diagnostic_does_not_hide_program_loss(self): log = "No DXBC: Unknown shader blob format.\n" with self.assertRaisesRegex(AssertionError, "Missing DirectX Vertex program 1"): From 3b07dab631d3420bda71568206c7ec92d9d9d53d Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 16:50:12 +0100 Subject: [PATCH 73/93] Refactor new code to follow project conventions --- .../Core/Hlsl/HlslAstBuilder.cs | 2 + .../Core/Hlsl/HlslPrettyPrinter.cs | 59 +++++- .../DXBC/IR/Builders/IRBuilderTexture.cs | 2 + .../DXBC/IR/IRBuilder.cs | 13 +- .../DXBC/IR/IRPipeline.cs | 2 + .../RawRegisterSemanticsTests.cs | 199 +++++++++++------- .../RegisterRoundTrip.cs | 161 +++++++++----- .../SaturatedMoveTests.cs | 7 +- .../StructuredBufferLoadsTests.cs | 83 +++++--- .../StructuredLoadCompatibilityTests.cs | 12 +- 10 files changed, 369 insertions(+), 171 deletions(-) diff --git a/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslAstBuilder.cs b/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslAstBuilder.cs index 0d60312..8818ab9 100644 --- a/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslAstBuilder.cs +++ b/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslAstBuilder.cs @@ -320,7 +320,9 @@ public static IEnumerable BuildResources( case IRDeclaration.IRResourceStructuredDeclaration structured: if (structured.StructureStride == 0 || structured.StructureStride % 4 != 0) + { throw new InvalidDataException($"Invalid structured resource stride {structured.StructureStride} at t{structured.Slot}."); + } yield return new HlslResourceNode { Name = $"t{structured.Slot}", diff --git a/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslPrettyPrinter.cs b/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslPrettyPrinter.cs index 299d97a..ef53f25 100644 --- a/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslPrettyPrinter.cs +++ b/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslPrettyPrinter.cs @@ -514,7 +514,9 @@ private static void PrintResource(StringBuilder sb, HlslResourceNode res, bool s .Append(res.Name).Append(regSuffix).Append(";\n"); } else + { sb.Append(" ").Append(res.TypeHint ?? "Texture2D").Append(' ').Append(res.Name).Append(regSuffix).Append(";\n"); + } break; case HlslResourceKind.Sampler: @@ -685,7 +687,9 @@ private static void PrintFunction( HlslSemanticNaming.Apply(fn.Statements, cbuffers, stageInputs, stageOutputs); if (fuseTemps && !fn.RawTemporaryStorage) + { HlslFuseTemps.Apply(fn.Statements, MaxFuseNodes); + } // Buffer the body so the Unity macro defines collected during // printing (ctx.Macros) can be emitted ahead of the statements that @@ -748,7 +752,9 @@ private static void PrintBlock(StringBuilder sb, HlslBlockStatement block, int i foreach (string line in ctx.PreserveArithmetic ? lines : CollapseExtractChains(RevertUnityMacros(lines, ctx), ctx)) + { sb.Append(pad).Append(line).Append('\n'); + } i = j; } else @@ -1950,7 +1956,9 @@ private static string RenderRegisterRead(IRRegister reg, PrintContext ctx) }; if (ctx.RawTemporaryStorage && reg.RegisterType is RegisterType.Temp or RegisterType.IndexableTemp) + { rendered = $"asfloat({rendered})"; + } return ApplyRegisterModifier(reg.Modifier, rendered); } @@ -2092,6 +2100,7 @@ private static IEnumerable RenderOutputWrite(IRRegister dest, string rhs yield return $"o.{FieldNameFor(dest, ctx)}{MaskOrSwizzleSuffix(dest)} = {OutputValue(rhs, elements?.FirstOrDefault(), ctx)};"; } + // Converts register bits to the declared output component type. private static string OutputValue(string bits, SignatureElement? element, PrintContext ctx) => !ctx.RawTemporaryStorage ? bits : element?.ComponentType switch { @@ -2165,7 +2174,9 @@ private static IEnumerable RenderAssignmentLines(IRRegister dest, IRExpr string rawRhs = RenderExpression(expr, ctx); if (ctx.RawTemporaryStorage && dest.RegisterType is RegisterType.Temp or RegisterType.IndexableTemp or RegisterType.Output) + { rawRhs = EncodeRegisterBits(rawRhs, expr); + } // DXBC computes full-lane expressions (the source operand's 4-wide // swizzle) regardless of how many components the destination @@ -2223,7 +2234,9 @@ private static IEnumerable RenderAssignmentLines(IRRegister dest, IRExpr ctx.DeclaredViews[(LocationOf(dest, c), v)] = (scalarName, new List { c }); string component = ExtractComponent(expr, c, ctx); if (ctx.RawTemporaryStorage) + { component = EncodeRegisterBits(component, expr); + } string line = $"{scalarName} = {component};"; yield return ctx.Declared.Add(scalarName) ? $"{DeclType(1, ctx)} {line}" : line; } @@ -2270,6 +2283,7 @@ private static string DeclType(int componentCount, PrintContext ctx) return componentCount == 1 ? type : $"{type}{componentCount}"; } + // Preserves the raw bits of values written to temporary registers. private static string EncodeRegisterBits(string value, IRExpression expression) => expression switch { IRExpression.RegisterExpression => $"asuint({value})", @@ -2597,7 +2611,9 @@ or IRExpression.BinaryOperation.LessThan private static string RenderBitwise(IRExpression.BinaryExpression b, PrintContext ctx) { if (ctx.RawTemporaryStorage) + { return $"({RenderExpression(b.Left, ctx)} {BinaryOpText(b.Operation)} {RenderExpression(b.Right, ctx)})"; + } string Op(IRExpression side) { string s = RenderExpression(side, ctx); @@ -2653,7 +2669,9 @@ private static string RenderIntrinsic(IRExpression.IntrinsicExpression expr, Pri // fxc folds frac(x + integer) even with /Gis, losing the add's rounding. // Use the DXBC definition of frc to retain that dependency. if (ctx.RawTemporaryStorage && expr.Intrinsic == IRExpression.IRIntrinsic.FractionalPart) + { return $"({Arg(0)} - floor({Arg(0)}))"; + } // Casts use C-style cast syntax, not a function call. switch (expr.Intrinsic) @@ -2765,7 +2783,9 @@ private static string RenderBitFieldInsert(IRExpression.BitFieldInsertExpression private static string RenderTextureOp(IRExpression.TextureOperationExpression tex, PrintContext ctx) { if (tex.Operation == IRExpression.TextureOperation.LoadStructured) + { return RenderStructuredLoad(tex, ctx); + } string resource = RenderRegisterRead(tex.Resource, ctx); string? sampler = tex.Sampler is null ? null : RenderRegisterRead(tex.Sampler, ctx); @@ -2820,22 +2840,15 @@ private static string RenderTextureOp(IRExpression.TextureOperationExpression te }; } + // Renders structured-buffer reads with their byte offsets and swizzles. private static string RenderStructuredLoad(IRExpression.TextureOperationExpression tex, PrintContext ctx) { - // Float temporaries can numerically convert integer outputs and lose payload bits. - if (!ctx.RawTemporaryStorage) - throw new InvalidDataException("Structured loads require raw register storage in the HLSL function."); - - if (tex.Coordinates is null || tex.Offset is null) - throw new InvalidDataException("Structured load requires an element index and a byte offset."); - uint? byteOffset = tex.Offset is IRExpression.ConstantExpression { RawValues.Length: 1 } offset - ? offset.RawValues[0] : null; - if (byteOffset.HasValue && byteOffset.Value % 4 != 0) - throw new InvalidDataException($"Unaligned structured load offset {byteOffset.Value}."); - + uint? byteOffset = ValidateStructuredLoad(tex, ctx); List lanes = MaskIndices(tex.DestinationMask); if (lanes.Count == 0) + { throw new InvalidDataException("Structured load has no destination lanes."); + } List swizzle = ActiveComponents(tex.Resource); List values = new List(); foreach (int lane in lanes) @@ -2845,7 +2858,9 @@ private static string RenderStructuredLoad(IRExpression.TextureOperationExpressi { uint index = byteOffset.Value / 4 + (uint)swizzle[lane]; if (index >= tex.StructuredStride / 4) + { throw new InvalidDataException($"Structured load exceeds element stride at t{tex.Resource.Index}."); + } word = index.ToString(System.Globalization.CultureInfo.InvariantCulture); } else @@ -2858,4 +2873,26 @@ private static string RenderStructuredLoad(IRExpression.TextureOperationExpressi ? $"asfloat({values[0]})" : $"asfloat(uint{lanes.Count}({string.Join(", ", values)}))"; } + + // Rejects unsupported storage and unaligned structured-load offsets. + private static uint? ValidateStructuredLoad(IRExpression.TextureOperationExpression tex, PrintContext ctx) + { + // Float temporaries can numerically convert integer outputs and lose payload bits. + if (!ctx.RawTemporaryStorage) + { + throw new InvalidDataException("Structured loads require raw register storage in the HLSL function."); + } + + if (tex.Coordinates is null || tex.Offset is null) + { + throw new InvalidDataException("Structured load requires an element index and a byte offset."); + } + uint? byteOffset = tex.Offset is IRExpression.ConstantExpression { RawValues.Length: 1 } offset + ? offset.RawValues[0] : null; + if (byteOffset.HasValue && byteOffset.Value % 4 != 0) + { + throw new InvalidDataException($"Unaligned structured load offset {byteOffset.Value}."); + } + return byteOffset; + } } diff --git a/src/UnityShaderDecompiler.Core/DXBC/IR/Builders/IRBuilderTexture.cs b/src/UnityShaderDecompiler.Core/DXBC/IR/Builders/IRBuilderTexture.cs index 014b6cb..ae8e6f8 100644 --- a/src/UnityShaderDecompiler.Core/DXBC/IR/Builders/IRBuilderTexture.cs +++ b/src/UnityShaderDecompiler.Core/DXBC/IR/Builders/IRBuilderTexture.cs @@ -102,7 +102,9 @@ private void BuildLdStructured(IRProgram program, Instruction instruction) uint stride = program.Declarations.OfType() .Single(d => d.Slot == resource.Index).StructureStride; if (stride == 0 || stride % 4 != 0) + { throw new InvalidDataException($"Invalid structured resource stride {stride} at t{resource.Index}."); + } AddAssignment(program, destination, new IRExpression.TextureOperationExpression { Operation = IRExpression.TextureOperation.LoadStructured, diff --git a/src/UnityShaderDecompiler.Core/DXBC/IR/IRBuilder.cs b/src/UnityShaderDecompiler.Core/DXBC/IR/IRBuilder.cs index 101ecfb..9925744 100644 --- a/src/UnityShaderDecompiler.Core/DXBC/IR/IRBuilder.cs +++ b/src/UnityShaderDecompiler.Core/DXBC/IR/IRBuilder.cs @@ -6,8 +6,10 @@ public partial class IRBuilder { private readonly bool _preserveRegisterBits; + // Initializes the requested register-storage mode. public IRBuilder() : this(false) { } + // Initializes the requested register-storage mode. public IRBuilder(bool preserveRegisterBits) => _preserveRegisterBits = preserveRegisterBits; private readonly Dictionary<(RegisterType Type, uint Index), IRValueType> _registerTypes = new(); @@ -519,22 +521,31 @@ private IRExpression BuildUIntExpression(Operand operand) return TypedRead(operand, IRExpression.IRIntrinsic.AsUInt); } + // Interprets a register value using the instruction operand type. private IRExpression TypedRead(Operand operand, IRExpression.IRIntrinsic interpretation) { IRRegister register = BuildRegister(operand); IRExpression value = new IRExpression.RegisterExpression { Register = register }; - if (!_preserveRegisterBits) return value; + if (!_preserveRegisterBits) + { + return value; + } ShdrParser.OperandModifier modifier = register.Modifier; register.Modifier = ShdrParser.OperandModifier.None; value = new IRExpression.IntrinsicExpression { Intrinsic = interpretation, Arguments = { value } }; // Integer negation is two's complement, not a float sign-bit flip. if (modifier is ShdrParser.OperandModifier.Abs or ShdrParser.OperandModifier.AbsNeg) + { value = new IRExpression.UnaryExpression { Operation = IRExpression.UnaryExpression.UnaryOperation.Absolute, Operand = value }; + } if (modifier is ShdrParser.OperandModifier.Neg or ShdrParser.OperandModifier.AbsNeg) + { value = new IRExpression.UnaryExpression { Operation = IRExpression.UnaryExpression.UnaryOperation.Negate, Operand = value }; + } return value; } + // Preserves bit interpretation when constructing an instruction expression. private IRExpression BuildRawExpression(Operand operand) => operand.RegisterType == RegisterType.Immediate32 ? BuildUIntExpression(operand) : new IRExpression.IntrinsicExpression diff --git a/src/UnityShaderDecompiler.Core/DXBC/IR/IRPipeline.cs b/src/UnityShaderDecompiler.Core/DXBC/IR/IRPipeline.cs index a0b2a3f..66fd858 100644 --- a/src/UnityShaderDecompiler.Core/DXBC/IR/IRPipeline.cs +++ b/src/UnityShaderDecompiler.Core/DXBC/IR/IRPipeline.cs @@ -83,7 +83,9 @@ void Snapshot(string phase, List blocks) // the others, but doing it here means it sees the same // simplified shape as everything else. if (!preserveArithmetic) + { IRShaderPatternRecognition.Run(blocks); + } Snapshot("05-pattern-recognized", blocks); List loops = IRShaderPatternRecognition.DetectLoops(blocks); diff --git a/tests/UnityShaderDecompiler.Tests/RawRegisterSemanticsTests.cs b/tests/UnityShaderDecompiler.Tests/RawRegisterSemanticsTests.cs index 8ecf057..480dde9 100644 --- a/tests/UnityShaderDecompiler.Tests/RawRegisterSemanticsTests.cs +++ b/tests/UnityShaderDecompiler.Tests/RawRegisterSemanticsTests.cs @@ -10,8 +10,10 @@ namespace UnityShaderDecompiler.Tests; +// Covers raw register semantics tests. public sealed class RawRegisterSemanticsTests { + // Verifies that integer moves preserve bits through native round trip. [Theory] [InlineData("uint", false)] [InlineData("int", false)] @@ -20,7 +22,10 @@ public sealed class RawRegisterSemanticsTests [Trait("Category", "D3D")] public void IntegerMovesPreserveBitsThroughNativeRoundTrip(string type, bool conditional) { - if (!OperatingSystem.IsWindows()) return; + if (!OperatingSystem.IsWindows()) + { + return; + } // These integers also encode NaNs, a subnormal and negative zero as floats. uint[] selected = { 0xffffffffu, 0x7fc12345u, 0x00000001u, 0x80000000u }; @@ -44,6 +49,7 @@ public void IntegerMovesPreserveBitsThroughNativeRoundTrip(string type, bool con } } + // Verifies that integer source modifier is applied after bit interpretation. [Fact] public void IntegerSourceModifierIsAppliedAfterBitInterpretation() { @@ -71,66 +77,22 @@ public void IntegerSourceModifierIsAppliedAfterBitInterpretation() Assert.Equal(ShdrParser.OperandModifier.None, register.Register.Modifier); } + // Verifies that fractional part retains preceding addition when recompiled. [Fact] [Trait("Category", "D3D")] public void FractionalPartRetainsPrecedingAdditionWhenRecompiled() { - if (!OperatingSystem.IsWindows()) return; - - UnityShaderDecompiler.Core.DXBC.Chunks.SignatureElement input = new UnityShaderDecompiler.Core.DXBC.Chunks.SignatureElement - { - SemanticName = "TEXCOORD", Mask = 1, ComponentType = 3, - }; - UnityShaderDecompiler.Core.DXBC.Chunks.SignatureElement output = new UnityShaderDecompiler.Core.DXBC.Chunks.SignatureElement - { - SemanticName = "SV_Target", Mask = 1, ComponentType = 3, - }; - HlslFunctionNode function = new HlslFunctionNode - { - Name = "frag", Stage = HlslShaderStage.Fragment, RawTemporaryStorage = true, - InputStruct = new HlslStructNode { Name = "TestInput" }, - OutputStruct = new HlslStructNode { Name = "TestOutput" }, - }; - function.InputStruct.Fields.Add(new HlslFieldNode - { - Name = HlslAstBuilder.ToFieldName("TEXCOORD0"), Semantic = "TEXCOORD0", TypeHint = "float", - }); - function.OutputStruct.Fields.Add(new HlslFieldNode - { - Name = HlslAstBuilder.ToFieldName("SV_Target0"), Semantic = "SV_Target0", TypeHint = "float", - }); - function.InputElementsByRegister[0] = new() { input }; - function.OutputElementsByRegister[0] = new() { output }; - IRRegister source = Temp(0); - source.RegisterType = RegisterType.Input; - source.SymbolicName = "TEXCOORD0"; - IRRegister destination = Temp(0); - destination.RegisterType = RegisterType.Output; - destination.SymbolicName = "SV_Target0"; - function.Statements.Statements.Add(new HlslAssignmentStatement - { - Destination = Temp(1), - Expression = new IRExpression.BinaryExpression - { - Operation = IRExpression.BinaryOperation.Add, - Left = new IRExpression.RegisterExpression { Register = source }, Right = Float(1), - }, - }); - function.Statements.Statements.Add(new HlslAssignmentStatement + if (!OperatingSystem.IsWindows()) { - Destination = destination, - Expression = new IRExpression.IntrinsicExpression - { - Intrinsic = IRExpression.IRIntrinsic.FractionalPart, - Arguments = { new IRExpression.RegisterExpression { Register = Temp(1) } }, - }, - }); - function.Statements.Statements.Add(new HlslReturnStatement()); + return; + } + + HlslFunctionNode function = BuildFractionFunction(); HlslShaderNode shader = new HlslShaderNode { Name = "Test/Fraction" }; HlslSubShaderNode subShader = new HlslSubShaderNode(); HlslPassNode pass = new HlslPassNode { FragmentFunction = function }; - pass.Structs.Add(function.InputStruct); - pass.Structs.Add(function.OutputStruct); + pass.Structs.Add(function.InputStruct!); + pass.Structs.Add(function.OutputStruct!); subShader.Passes.Add(pass); shader.SubShaders.Add(subShader); string text = HlslPrettyPrinter.Print(shader, fuseTemps: false); @@ -147,12 +109,15 @@ public void FractionalPartRetainsPrecedingAdditionWhenRecompiled() Assert.Contains(instructions.Skip(floor + 1), i => i.Opcode == Opcode.Add); } + // Verifies that raw temporary storage keeps comparison masks and integer arithmetic. [Fact] public void RawTemporaryStorageKeepsComparisonMasksAndIntegerArithmetic() { HlslFunctionNode function = new HlslFunctionNode { - Name = "frag", Stage = HlslShaderStage.Fragment, RawTemporaryStorage = true, + Name = "frag", + Stage = HlslShaderStage.Fragment, + RawTemporaryStorage = true, }; function.Statements.Statements.Add(new HlslAssignmentStatement { @@ -160,27 +125,11 @@ public void RawTemporaryStorageKeepsComparisonMasksAndIntegerArithmetic() Expression = new IRExpression.BinaryExpression { Operation = IRExpression.BinaryOperation.LessThan, - Left = Float(1), Right = Float(2), - }, - }); - IRExpression.IntrinsicExpression signedRead = new IRExpression.IntrinsicExpression - { - Intrinsic = IRExpression.IRIntrinsic.AsInt, - Arguments = { new IRExpression.RegisterExpression { Register = Temp(0) } }, - }; - function.Statements.Statements.Add(new HlslAssignmentStatement - { - Destination = Temp(1), - Expression = new IRExpression.BinaryExpression - { - Operation = IRExpression.BinaryOperation.Add, - Left = signedRead, - Right = new IRExpression.ConstantExpression - { - Kind = IRExpression.ConstantExpression.ConstantKind.Int, RawValues = new uint[] { 2 }, - }, + Left = Float(1), + Right = Float(2), }, }); + AddSignedAddition(function); string source = Print(function); Assert.Contains("uint r0_x_1", source); Assert.Contains("? 0xffffffffu : 0u", source); @@ -188,6 +137,7 @@ public void RawTemporaryStorageKeepsComparisonMasksAndIntegerArithmetic() Assert.Contains("uint r1_x_1 = asuint(", source); } + // Wraps the fixture function in a shader and emits its HLSL. private static string Print(HlslFunctionNode function) { HlslShaderNode shader = new HlslShaderNode { Name = "Test/Scope" }; @@ -197,6 +147,7 @@ private static string Print(HlslFunctionNode function) return HlslPrettyPrinter.Print(shader, fuseTemps: false); } + // Creates a temporary register with the requested index and lane mask. private static IRRegister Temp(uint index, byte mask = 1) { IRRegister register = new IRRegister @@ -208,13 +159,115 @@ private static IRRegister Temp(uint index, byte mask = 1) ComponentMode = Operand.OperandComponentMode.Mask, }; for (int component = 0; component < 4; component++) + { if ((mask & (1 << component)) != 0) + { register.SsaVersion[component] = 1; + } + } return register; } + // Creates a scalar floating-point constant for the fixture. private static IRExpression.ConstantExpression Float(float value) => new() { RawValues = new[] { unchecked((uint)BitConverter.SingleToInt32Bits(value)) }, }; + + // Builds a fragment function that computes frac(x + 1). + private static HlslFunctionNode BuildFractionFunction() + { + HlslFunctionNode function = CreateFractionInterface(); + IRRegister source = Temp(0); + source.RegisterType = RegisterType.Input; + source.SymbolicName = "TEXCOORD0"; + IRRegister destination = Temp(0); + destination.RegisterType = RegisterType.Output; + destination.SymbolicName = "SV_Target0"; + function.Statements.Statements.Add(new HlslAssignmentStatement + { + Destination = Temp(1), + Expression = new IRExpression.BinaryExpression + { + Operation = IRExpression.BinaryOperation.Add, + Left = new IRExpression.RegisterExpression { Register = source }, + Right = Float(1), + }, + }); + function.Statements.Statements.Add(new HlslAssignmentStatement + { + Destination = destination, + Expression = new IRExpression.IntrinsicExpression + { + Intrinsic = IRExpression.IRIntrinsic.FractionalPart, + Arguments = { new IRExpression.RegisterExpression { Register = Temp(1) } }, + }, + }); + function.Statements.Statements.Add(new HlslReturnStatement()); + return function; + } + + // Creates matching scalar input and output interfaces for the fraction fixture. + private static HlslFunctionNode CreateFractionInterface() + { + UnityShaderDecompiler.Core.DXBC.Chunks.SignatureElement input = new UnityShaderDecompiler.Core.DXBC.Chunks.SignatureElement + { + SemanticName = "TEXCOORD", + Mask = 1, + ComponentType = 3, + }; + UnityShaderDecompiler.Core.DXBC.Chunks.SignatureElement output = new UnityShaderDecompiler.Core.DXBC.Chunks.SignatureElement + { + SemanticName = "SV_Target", + Mask = 1, + ComponentType = 3, + }; + HlslFunctionNode function = new HlslFunctionNode + { + Name = "frag", + Stage = HlslShaderStage.Fragment, + RawTemporaryStorage = true, + InputStruct = new HlslStructNode { Name = "TestInput" }, + OutputStruct = new HlslStructNode { Name = "TestOutput" }, + }; + function.InputStruct.Fields.Add(new HlslFieldNode + { + Name = HlslAstBuilder.ToFieldName("TEXCOORD0"), + Semantic = "TEXCOORD0", + TypeHint = "float", + }); + function.OutputStruct.Fields.Add(new HlslFieldNode + { + Name = HlslAstBuilder.ToFieldName("SV_Target0"), + Semantic = "SV_Target0", + TypeHint = "float", + }); + function.InputElementsByRegister[0] = new() { input }; + function.OutputElementsByRegister[0] = new() { output }; + return function; + } + + // Adds signed integer arithmetic after a raw comparison-mask read. + private static void AddSignedAddition(HlslFunctionNode function) + { + IRExpression.IntrinsicExpression signedRead = new IRExpression.IntrinsicExpression + { + Intrinsic = IRExpression.IRIntrinsic.AsInt, + Arguments = { new IRExpression.RegisterExpression { Register = Temp(0) } }, + }; + function.Statements.Statements.Add(new HlslAssignmentStatement + { + Destination = Temp(1), + Expression = new IRExpression.BinaryExpression + { + Operation = IRExpression.BinaryOperation.Add, + Left = signedRead, + Right = new IRExpression.ConstantExpression + { + Kind = IRExpression.ConstantExpression.ConstantKind.Int, + RawValues = new uint[] { 2 }, + }, + }, + }); + } } diff --git a/tests/UnityShaderDecompiler.Tests/RegisterRoundTrip.cs b/tests/UnityShaderDecompiler.Tests/RegisterRoundTrip.cs index 971fc98..33f145c 100644 --- a/tests/UnityShaderDecompiler.Tests/RegisterRoundTrip.cs +++ b/tests/UnityShaderDecompiler.Tests/RegisterRoundTrip.cs @@ -13,8 +13,10 @@ namespace UnityShaderDecompiler.Tests; +// Covers register round trip. internal static class RegisterRoundTrip { + // Compiles the fixture HLSL into a DXBC container. internal static DxbcFile Compile(string source, string entry = "main") { DxbcFile file = new DxbcFile(); @@ -22,6 +24,7 @@ internal static DxbcFile Compile(string source, string entry = "main") return file; } + // Decompiles and recompiles a fixture without changing its register bits. internal static DxbcFile Recompile(DxbcFile original, bool fuseTemps = false) { IRPipeline.Result pipeline = IRPipeline.Run(original, dumpStages: false, preserveArithmetic: !fuseTemps); @@ -54,69 +57,20 @@ internal static uint[] Evaluate(DxbcFile file, uint[] input, uint[]? structuredW }; foreach (Instruction instruction in file.Shader!.Instructions) { - if (instruction.Opcode.ToString().StartsWith("Dcl", StringComparison.Ordinal)) continue; - if (instruction.Opcode == Opcode.Ret) return registers[(RegisterType.Output, 0)]; - Assert.True(instruction.Opcode is Opcode.Mov or Opcode.MovC or Opcode.INe or Opcode.IEq - or Opcode.And or Opcode.LdStructured or Opcode.IAdd or Opcode.Ishl or Opcode.Ushr or Opcode.Bfi, - $"Unexpected opcode: {instruction.Opcode}"); - Operand destination = instruction.Operands[0]; - uint[] result = new uint[4]; - for (int lane = 0; lane < 4; lane++) + if (instruction.Opcode.ToString().StartsWith("Dcl", StringComparison.Ordinal)) { - if ((destination.Mask & (1 << lane)) == 0) continue; - uint first = Read(instruction.Operands[1], lane, registers); - if (instruction.Opcode == Opcode.Mov) result[lane] = first; - else if (instruction.Opcode == Opcode.MovC) - result[lane] = Read(instruction.Operands[first != 0 ? 2 : 3], lane, registers); - else if (instruction.Opcode == Opcode.Bfi) - { - int width = (int)(first & 31); - int offset = (int)(Read(instruction.Operands[2], lane, registers) & 31); - uint mask = unchecked(((1u << width) - 1u) << offset); - uint insert = Read(instruction.Operands[3], lane, registers); - uint original = Read(instruction.Operands[4], lane, registers); - result[lane] = ((insert << offset) & mask) | (original & ~mask); - } - else if (instruction.Opcode == Opcode.LdStructured) - { - Assert.NotNull(structuredWords); - Operand resource = instruction.Operands[3]; - Instruction declaration = Assert.Single(file.Shader.Instructions, - item => item.Opcode == Opcode.DclResourceStructured - && item.Operands[0].RegisterIndex == resource.RegisterIndex); - uint stride = declaration.ExtraData.Single(); - uint offset = Read(instruction.Operands[2], lane, registers); - Assert.Equal(0u, offset % 4); - result[lane] = structuredWords[checked((int)((first * stride + offset) / 4)) + Component(resource, lane)]; - } - else - { - uint second = Read(instruction.Operands[2], lane, registers); - result[lane] = instruction.Opcode switch - { - Opcode.INe => first != second ? uint.MaxValue : 0u, - Opcode.IEq => first == second ? uint.MaxValue : 0u, - Opcode.And => first & second, - Opcode.IAdd => unchecked(first + second), - Opcode.Ishl => first << (int)(second & 31), - Opcode.Ushr => first >> (int)(second & 31), - _ => throw new InvalidOperationException(), - }; - } - if (instruction.Saturate) - { - float value = BitConverter.UInt32BitsToSingle(result[lane]); - result[lane] = BitConverter.SingleToUInt32Bits(float.IsNaN(value) ? 0f : Math.Clamp(value, 0f, 1f)); - } + continue; } - (RegisterType, uint) key = (destination.RegisterType, destination.RegisterIndex); - if (!registers.TryGetValue(key, out uint[]? target)) registers[key] = target = new uint[4]; - for (int lane = 0; lane < 4; lane++) - if ((destination.Mask & (1 << lane)) != 0) target[lane] = result[lane]; + if (instruction.Opcode == Opcode.Ret) + { + return registers[(RegisterType.Output, 0)]; + } + ExecuteInstruction(file, instruction, registers, structuredWords); } throw new InvalidOperationException("Fixture has no return instruction."); } + // Selects the source component requested by an operand. private static int Component(Operand operand, int lane) => operand.ComponentMode switch { Operand.OperandComponentMode.Select1 => operand.Component, @@ -124,12 +78,105 @@ internal static uint[] Evaluate(DxbcFile file, uint[] input, uint[]? structuredW _ => lane, }; + // Reads the selected operand lane without applying numeric conversion. private static uint Read(Operand operand, int lane, Dictionary<(RegisterType, uint), uint[]> registers) { Assert.Equal(ShdrParser.OperandModifier.None, operand.Modifier); int component = Component(operand, lane); if (operand.RegisterType == RegisterType.Immediate32) + { return operand.Immediate32Values![operand.Immediate32Values.Length == 1 ? 0 : component]; + } return registers[(operand.RegisterType, operand.RegisterIndex)][component]; } + + // Executes one supported fixture instruction and writes its masked result. + private static void ExecuteInstruction(DxbcFile file, Instruction instruction, Dictionary<(RegisterType, uint), uint[]> registers, uint[]? structuredWords) + { + Assert.True(instruction.Opcode is Opcode.Mov or Opcode.MovC or Opcode.INe or Opcode.IEq + or Opcode.And or Opcode.LdStructured or Opcode.IAdd or Opcode.Ishl or Opcode.Ushr or Opcode.Bfi, + $"Unexpected opcode: {instruction.Opcode}"); + Operand destination = instruction.Operands[0]; + uint[] result = new uint[4]; + for (int lane = 0; lane < 4; lane++) + { + if ((destination.Mask & (1 << lane)) == 0) + { + continue; + } + result[lane] = EvaluateLane(file, instruction, lane, registers, structuredWords); + if (instruction.Saturate) + { + float value = BitConverter.UInt32BitsToSingle(result[lane]); + result[lane] = BitConverter.SingleToUInt32Bits(float.IsNaN(value) ? 0f : Math.Clamp(value, 0f, 1f)); + } + } + (RegisterType, uint) key = (destination.RegisterType, destination.RegisterIndex); + if (!registers.TryGetValue(key, out uint[]? target)) + { + registers[key] = target = new uint[4]; + } + for (int lane = 0; lane < 4; lane++) + { + if ((destination.Mask & (1 << lane)) != 0) + { + target[lane] = result[lane]; + } + } + } + + // Evaluates one lane without changing its raw register bits. + private static uint EvaluateLane(DxbcFile file, Instruction instruction, int lane, Dictionary<(RegisterType, uint), uint[]> registers, uint[]? structuredWords) + { + uint first = Read(instruction.Operands[1], lane, registers); + if (instruction.Opcode == Opcode.Mov) + { + return first; + } + else if (instruction.Opcode == Opcode.MovC) + { + return Read(instruction.Operands[first != 0 ? 2 : 3], lane, registers); + } + else if (instruction.Opcode == Opcode.Bfi) + { + int width = (int)(first & 31); + int offset = (int)(Read(instruction.Operands[2], lane, registers) & 31); + uint mask = unchecked(((1u << width) - 1u) << offset); + uint insert = Read(instruction.Operands[3], lane, registers); + uint original = Read(instruction.Operands[4], lane, registers); + return ((insert << offset) & mask) | (original & ~mask); + } + else if (instruction.Opcode == Opcode.LdStructured) + { + return ReadStructuredWord(file, instruction, lane, registers, structuredWords, first); + } + else + { + uint second = Read(instruction.Operands[2], lane, registers); + return instruction.Opcode switch + { + Opcode.INe => first != second ? uint.MaxValue : 0u, + Opcode.IEq => first == second ? uint.MaxValue : 0u, + Opcode.And => first & second, + Opcode.IAdd => unchecked(first + second), + Opcode.Ishl => first << (int)(second & 31), + Opcode.Ushr => first >> (int)(second & 31), + _ => throw new InvalidOperationException(), + }; + } + } + + // Reads one structured-buffer word using the serialized stride and offset. + private static uint ReadStructuredWord(DxbcFile file, Instruction instruction, int lane, Dictionary<(RegisterType, uint), uint[]> registers, uint[]? structuredWords, uint first) + { + Assert.NotNull(structuredWords); + Operand resource = instruction.Operands[3]; + Instruction declaration = Assert.Single(file.Shader!.Instructions, + item => item.Opcode == Opcode.DclResourceStructured + && item.Operands[0].RegisterIndex == resource.RegisterIndex); + uint stride = declaration.ExtraData.Single(); + uint offset = Read(instruction.Operands[2], lane, registers); + Assert.Equal(0u, offset % 4); + return structuredWords[checked((int)((first * stride + offset) / 4)) + Component(resource, lane)]; + } } diff --git a/tests/UnityShaderDecompiler.Tests/SaturatedMoveTests.cs b/tests/UnityShaderDecompiler.Tests/SaturatedMoveTests.cs index aec73a4..6319d6c 100644 --- a/tests/UnityShaderDecompiler.Tests/SaturatedMoveTests.cs +++ b/tests/UnityShaderDecompiler.Tests/SaturatedMoveTests.cs @@ -3,15 +3,20 @@ namespace UnityShaderDecompiler.Tests; +// Covers saturated move tests. public sealed class SaturatedMoveTests { + // Verifies that saturated moves clamp float values before storing bits. [Theory] [InlineData(false)] [InlineData(true)] [Trait("Category", "D3D")] public void SaturatedMovesClampFloatValuesBeforeStoringBits(bool conditional) { - if (!OperatingSystem.IsWindows()) return; + if (!OperatingSystem.IsWindows()) + { + return; + } string selection = conditional ? "((asuint(value.y) & 1u) != 0u ? value : float4(-0.5, 0.25, 1.5, 0))" : "value"; DxbcFile original = RegisterRoundTrip.Compile( $"uint4 main(float4 value:TEXCOORD0):SV_Target {{ return asuint(saturate({selection})); }}"); diff --git a/tests/UnityShaderDecompiler.Tests/StructuredBufferLoadsTests.cs b/tests/UnityShaderDecompiler.Tests/StructuredBufferLoadsTests.cs index db51fa2..0050108 100644 --- a/tests/UnityShaderDecompiler.Tests/StructuredBufferLoadsTests.cs +++ b/tests/UnityShaderDecompiler.Tests/StructuredBufferLoadsTests.cs @@ -10,8 +10,10 @@ namespace UnityShaderDecompiler.Tests; +// Covers structured buffer loads tests. public sealed class StructuredBufferLoadsTests { + // Verifies that structured load preserves words through native round trip. [Theory] [InlineData("float")] [InlineData("uint")] @@ -19,7 +21,10 @@ public sealed class StructuredBufferLoadsTests [Trait("Category", "D3D")] public void StructuredLoadPreservesWordsThroughNativeRoundTrip(string type) { - if (!OperatingSystem.IsWindows()) return; + if (!OperatingSystem.IsWindows()) + { + return; + } string source = $"struct Element {{ uint4 padding; {type}4 value; }}; " + $"StructuredBuffer data : register(t7); " + @@ -46,36 +51,14 @@ public void StructuredLoadPreservesWordsThroughNativeRoundTrip(string type) } } + // Verifies that structured load uses byte offset and resource swizzle. [Theory] [InlineData(4u, 32u, true)] [InlineData(2u, 32u, false)] [InlineData(32u, 32u, false)] public void StructuredLoadUsesByteOffsetAndResourceSwizzle(uint offset, uint stride, bool valid) { - IRRegister resource = new() - { - RegisterType = RegisterType.Resource, Index = 7, - ComponentMode = Operand.OperandComponentMode.Swizzle, - Swizzle = 2 | (0 << 2) | (1 << 4) | (3 << 6), - }; - HlslFunctionNode function = new() { Name = "frag", Stage = HlslShaderStage.Fragment }; - function.Statements.Statements.Add(new HlslAssignmentStatement - { - Destination = Temp(0, 3), - Expression = new IRExpression.TextureOperationExpression - { - Operation = IRExpression.TextureOperation.LoadStructured, - Resource = resource, StructuredStride = stride, DestinationMask = 3, - Coordinates = new IRExpression.ConstantExpression - { - Kind = IRExpression.ConstantExpression.ConstantKind.UInt, RawValues = new uint[] { 5 }, - }, - Offset = new IRExpression.ConstantExpression - { - Kind = IRExpression.ConstantExpression.ConstantKind.UInt, RawValues = new uint[] { offset }, - }, - }, - }); + HlslFunctionNode function = BuildStructuredRead(offset, stride); if (valid) { string source = Print(function); @@ -87,6 +70,7 @@ public void StructuredLoadUsesByteOffsetAndResourceSwizzle(uint offset, uint str } } + // Verifies that structured buffer declaration uses its actual stride. [Fact] public void StructuredBufferDeclarationUsesItsActualStride() { @@ -95,8 +79,11 @@ public void StructuredBufferDeclarationUsesItsActualStride() HlslPassNode pass = new HlslPassNode(); pass.Resources.Add(new HlslResourceNode { - Name = "t7", Kind = HlslResourceKind.Texture, Slot = 7, - TypeHint = "StructuredBuffer", StructureStride = 32, + Name = "t7", + Kind = HlslResourceKind.Texture, + Slot = 7, + TypeHint = "StructuredBuffer", + StructureStride = 32, }); subShader.Passes.Add(pass); shader.SubShaders.Add(subShader); @@ -106,6 +93,7 @@ public void StructuredBufferDeclarationUsesItsActualStride() Assert.Contains("StructuredBuffer t7", source); } + // Wraps the fixture function in a shader and emits its HLSL. private static string Print(HlslFunctionNode function) { function.RawTemporaryStorage = true; @@ -116,6 +104,7 @@ private static string Print(HlslFunctionNode function) return HlslPrettyPrinter.Print(shader, fuseTemps: false); } + // Creates a temporary register with the requested index and lane mask. private static IRRegister Temp(uint index, byte mask = 1) { IRRegister register = new IRRegister @@ -127,13 +116,53 @@ private static IRRegister Temp(uint index, byte mask = 1) ComponentMode = Operand.OperandComponentMode.Mask, }; for (int component = 0; component < 4; component++) + { if ((mask & (1 << component)) != 0) + { register.SsaVersion[component] = 1; + } + } return register; } + // Creates a scalar floating-point constant for the fixture. private static IRExpression.ConstantExpression Float(float value) => new() { RawValues = new[] { unchecked((uint)BitConverter.SingleToInt32Bits(value)) }, }; + + // Builds a structured load with the supplied byte offset and resource swizzle. + private static HlslFunctionNode BuildStructuredRead(uint offset, uint stride) + { + IRRegister resource = new() + { + RegisterType = RegisterType.Resource, + Index = 7, + ComponentMode = Operand.OperandComponentMode.Swizzle, + Swizzle = 2 | (0 << 2) | (1 << 4) | (3 << 6), + }; + HlslFunctionNode function = new() { Name = "frag", Stage = HlslShaderStage.Fragment }; + function.Statements.Statements.Add(new HlslAssignmentStatement + { + Destination = Temp(0, 3), + Expression = new IRExpression.TextureOperationExpression + { + Operation = IRExpression.TextureOperation.LoadStructured, + Resource = resource, + StructuredStride = stride, + DestinationMask = 3, + Coordinates = new IRExpression.ConstantExpression + { + Kind = IRExpression.ConstantExpression.ConstantKind.UInt, + RawValues = new uint[] { 5 }, + }, + Offset = new IRExpression.ConstantExpression + { + Kind = IRExpression.ConstantExpression.ConstantKind.UInt, + RawValues = new uint[] { offset }, + }, + }, + }); + return function; + } } diff --git a/tests/UnityShaderDecompiler.Tests/StructuredLoadCompatibilityTests.cs b/tests/UnityShaderDecompiler.Tests/StructuredLoadCompatibilityTests.cs index 0341c92..4c6a187 100644 --- a/tests/UnityShaderDecompiler.Tests/StructuredLoadCompatibilityTests.cs +++ b/tests/UnityShaderDecompiler.Tests/StructuredLoadCompatibilityTests.cs @@ -4,8 +4,10 @@ namespace UnityShaderDecompiler.Tests; +// Covers structured load compatibility tests. public sealed class StructuredLoadCompatibilityTests { + // Verifies that existing public entry points and enum values remain available. [Fact] public void ExistingPublicEntryPointsAndEnumValuesRemainAvailable() { @@ -15,6 +17,7 @@ public void ExistingPublicEntryPointsAndEnumValuesRemainAvailable() Assert.Equal(14, (int)IRExpression.TextureOperation.CheckAccessFullyMapped); } + // Verifies that structured loads keep exact words with default options and dynamic offsets. [Theory] [InlineData(true, false)] [InlineData(false, false)] @@ -23,17 +26,23 @@ public void ExistingPublicEntryPointsAndEnumValuesRemainAvailable() [Trait("Category", "D3D")] public void StructuredLoadsKeepExactWordsWithDefaultOptionsAndDynamicOffsets(bool fuseTemps, bool dynamicOffset) { - if (!OperatingSystem.IsWindows()) return; + if (!OperatingSystem.IsWindows()) + { + return; + } string member = dynamicOffset ? "i.y & 3" : "2"; string source = "struct Element { uint value[4]; }; StructuredBuffer data : register(t7); " + $"uint4 main(nointerpolation uint2 i : TEXCOORD0) : SV_Target {{ return data[i.x].value[{member}]; }}"; DxbcFile original = RegisterRoundTrip.Compile(source); if (dynamicOffset) + { Assert.Contains(original.Shader!.Instructions, instruction => instruction.Opcode == Opcode.LdStructured && instruction.Operands[2].RegisterType != RegisterType.Immediate32); + } DxbcFile rebuilt = RegisterRoundTrip.Recompile(original, fuseTemps); uint[] words = { 1, 0xffffffffu, 0x7fc12345u, 0x80000000u, 0x01000001u, 0xffc54321u, 0x7fffffffu, 0x80000001u }; for (uint index = 0; index < 2; index++) + { for (uint component = 0; component < 4; component++) { uint[] input = { index, component, 0, 0 }; @@ -42,5 +51,6 @@ public void StructuredLoadsKeepExactWordsWithDefaultOptionsAndDynamicOffsets(boo Assert.Equal(output, RegisterRoundTrip.Evaluate(original, input, words)); Assert.Equal(output, RegisterRoundTrip.Evaluate(rebuilt, input, words)); } + } } } From 3ee7c4b587093cfa9446b1c58642945966937e90 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 16:50:12 +0100 Subject: [PATCH 74/93] Refactor new code to follow project conventions --- .../TemporaryRegisterNamesTests.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tests/UnityShaderDecompiler.Tests/TemporaryRegisterNamesTests.cs b/tests/UnityShaderDecompiler.Tests/TemporaryRegisterNamesTests.cs index 74faecf..7196d94 100644 --- a/tests/UnityShaderDecompiler.Tests/TemporaryRegisterNamesTests.cs +++ b/tests/UnityShaderDecompiler.Tests/TemporaryRegisterNamesTests.cs @@ -10,8 +10,10 @@ namespace UnityShaderDecompiler.Tests; +// Covers temporary register names tests. public sealed class TemporaryRegisterNamesTests { + // Verifies that semantic names do not merge different temporary registers. [Fact] public void SemanticNamesDoNotMergeDifferentTemporaryRegisters() { @@ -22,11 +24,13 @@ public void SemanticNamesDoNotMergeDifferentTemporaryRegisters() HlslFunctionNode function = new HlslFunctionNode { Name = "frag", Stage = HlslShaderStage.Fragment }; function.Statements.Statements.Add(new HlslAssignmentStatement { - Destination = first, Expression = Float(2), + Destination = first, + Expression = Float(2), }); function.Statements.Statements.Add(new HlslAssignmentStatement { - Destination = second, Expression = Float(3), + Destination = second, + Expression = Float(3), }); function.Statements.Statements.Add(new HlslAssignmentStatement { @@ -41,6 +45,7 @@ public void SemanticNamesDoNotMergeDifferentTemporaryRegisters() } + // Wraps the fixture function in a shader and emits its HLSL. private static string Print(HlslFunctionNode function) { HlslShaderNode shader = new HlslShaderNode { Name = "Test/Scope" }; @@ -50,6 +55,7 @@ private static string Print(HlslFunctionNode function) return HlslPrettyPrinter.Print(shader, fuseTemps: false); } + // Creates a temporary register with the requested index and lane mask. private static IRRegister Temp(uint index, byte mask = 1) { IRRegister register = new IRRegister @@ -61,11 +67,16 @@ private static IRRegister Temp(uint index, byte mask = 1) ComponentMode = Operand.OperandComponentMode.Mask, }; for (int component = 0; component < 4; component++) + { if ((mask & (1 << component)) != 0) + { register.SsaVersion[component] = 1; + } + } return register; } + // Creates a scalar floating-point constant for the fixture. private static IRExpression.ConstantExpression Float(float value) => new() { RawValues = new[] { unchecked((uint)BitConverter.SingleToInt32Bits(value)) }, From 33875e22c82ff98456f007c16cfd20f8d3c78c0d Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 16:50:12 +0100 Subject: [PATCH 75/93] Refactor new code to follow project conventions --- .../Core/Hlsl/HlslPrettyPrinter.cs | 4 ++++ .../VectorExpressionWidthsTests.cs | 11 +++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslPrettyPrinter.cs b/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslPrettyPrinter.cs index f9432ff..408ef07 100644 --- a/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslPrettyPrinter.cs +++ b/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslPrettyPrinter.cs @@ -2509,10 +2509,13 @@ private static bool IsLeading(List components) return true; } + // Renders a dot product using only its active vector lanes. private static string RenderDotProduct(IRExpression.DotProductExpression dot, PrintContext ctx) { if (dot.Components is < 2 or > 4) + { throw new InvalidOperationException($"Unsupported dot-product width: {dot.Components}"); + } string lanes = "xyzw"[..dot.Components]; return $"dot(({RenderExpression(dot.Left, ctx)}).{lanes}, ({RenderExpression(dot.Right, ctx)}).{lanes})"; } @@ -2609,6 +2612,7 @@ private static string RenderIntrinsic(IRExpression.IntrinsicExpression expr, Pri case IRExpression.IRIntrinsic.CastBool: return $"({CastType("bool")})({Arg(0)})"; } + // Preserves the vector width when rendering a typed cast. string CastType(string scalar) { int width = ExpressionWidth(expr.Arguments[0], ctx); diff --git a/tests/UnityShaderDecompiler.Tests/VectorExpressionWidthsTests.cs b/tests/UnityShaderDecompiler.Tests/VectorExpressionWidthsTests.cs index ca4670d..89b4563 100644 --- a/tests/UnityShaderDecompiler.Tests/VectorExpressionWidthsTests.cs +++ b/tests/UnityShaderDecompiler.Tests/VectorExpressionWidthsTests.cs @@ -10,8 +10,10 @@ namespace UnityShaderDecompiler.Tests; +// Covers vector expression widths tests. public sealed class VectorExpressionWidthsTests { + // Verifies that normalize simplification keeps the dot product lanes. [Fact] public void NormalizeSimplificationKeepsTheDotProductLanes() { @@ -24,6 +26,7 @@ public void NormalizeSimplificationKeepsTheDotProductLanes() Assert.DoesNotMatch(@"normalize\(cb1_values\s*\[5\]\)", simplified); } + // Verifies that three component dot does not read the fourth lane. [Fact] public void ThreeComponentDotDoesNotReadTheFourthLane() { @@ -43,6 +46,7 @@ public void ThreeComponentDotDoesNotReadTheFourthLane() Assert.Matches(@"dot\(\(.+\)\.xyz, \(.+\)\.xyz\)", source); } + // Verifies that vector cast preserves width for component access. [Fact] public void VectorCastPreservesWidthForComponentAccess() { @@ -66,6 +70,7 @@ public void VectorCastPreservesWidthForComponentAccess() Assert.DoesNotContain("(float)r0_xyzw_1", source); } + // Wraps the fixture function in a shader and emits its HLSL. private static string Print(HlslFunctionNode function) { HlslShaderNode shader = new HlslShaderNode { Name = "Test/Scope" }; @@ -75,6 +80,7 @@ private static string Print(HlslFunctionNode function) return HlslPrettyPrinter.Print(shader, fuseTemps: false); } + // Creates a temporary register with the requested index and lane mask. private static IRRegister Temp(uint index, byte mask = 1) { IRRegister register = new IRRegister @@ -86,11 +92,16 @@ private static IRRegister Temp(uint index, byte mask = 1) ComponentMode = Operand.OperandComponentMode.Mask, }; for (int component = 0; component < 4; component++) + { if ((mask & (1 << component)) != 0) + { register.SsaVersion[component] = 1; + } + } return register; } + // Creates a scalar floating-point constant for the fixture. private static IRExpression.ConstantExpression Float(float value) => new() { RawValues = new[] { unchecked((uint)BitConverter.SingleToInt32Bits(value)) }, From 5820ac4d33105ab4d8799b315f188de51812bcca Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 16:50:12 +0100 Subject: [PATCH 76/93] Refactor new code to follow project conventions --- libs/metadata.py | 4 +- .../Core/Hlsl/HlslPrettyPrinter.cs | 86 +++++---- .../DXBC/Metadata/ProgramVariantMetadata.cs | 4 + .../DXBC/Metadata/Shadermetadata.cs | 10 +- src/UnityShaderDecompiler.Core/Decompiler.cs | 166 +++++++++++----- .../RuntimeCompilationCase.cs | 5 + .../RuntimeKeywordGroup.cs | 4 + .../RuntimeVariantManifest.cs | 58 ++++-- .../RuntimeVariantPlan.cs | 91 +++++++-- .../RuntimeVariantState.cs | 4 + .../VariantPairing.cs | 109 ++++++++--- tests/UnityRuntime/README.md | 12 +- tests/UnityRuntime/RuntimeKeywordChecks.cs | 141 +++++++++----- tests/UnityRuntime/ShaderVariantChecks.cs | 133 ++++++++----- tests/UnityRuntime/ShaderVariantData.cs | 8 + .../UnityRuntime/ShaderVariantManifestData.cs | 10 + tests/UnityRuntime/ShaderVariantPassData.cs | 9 + .../RuntimeVariantEmissionTests.cs | 181 ++++++++++++------ .../RuntimeVariantPlanTests.cs | 16 +- .../VariantInterfaceTests.cs | 12 +- .../VariantPairingTests.cs | 6 + tests/test_keyword_flags.py | 6 + tests/test_variant_metadata.py | 4 + 23 files changed, 759 insertions(+), 320 deletions(-) create mode 100644 src/UnityShaderDecompiler.Core/DXBC/Metadata/ProgramVariantMetadata.cs create mode 100644 src/UnityShaderDecompiler.Core/RuntimeCompilationCase.cs create mode 100644 src/UnityShaderDecompiler.Core/RuntimeKeywordGroup.cs create mode 100644 src/UnityShaderDecompiler.Core/RuntimeVariantState.cs create mode 100644 tests/UnityRuntime/ShaderVariantData.cs create mode 100644 tests/UnityRuntime/ShaderVariantManifestData.cs create mode 100644 tests/UnityRuntime/ShaderVariantPassData.cs diff --git a/libs/metadata.py b/libs/metadata.py index e1f5a16..ddd62d2 100644 --- a/libs/metadata.py +++ b/libs/metadata.py @@ -163,6 +163,7 @@ def _pass(self, p, name_layouts=None, keyword_names=()): "keywords": p.get("m_NameIndices", []), } + # Preserves local/global keyword scope from serialized flags. @staticmethod def _keyword_flags(parsed): flags = parsed.get("m_KeywordFlags") @@ -177,6 +178,7 @@ def _keyword_flags(parsed): raise ValueError("Invalid serialized shader keyword flags") return dict(zip(names, flags)) + # Extracts DirectX stage references and keyword masks. @staticmethod def _directx_programs(shader_pass, keyword_names): result = [] @@ -386,4 +388,4 @@ def _tags(tags): if isinstance(entries, dict): for k, v in entries.items(): out[k] = v.get("second", v) if isinstance(v, dict) else v - return out \ No newline at end of file + return out diff --git a/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslPrettyPrinter.cs b/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslPrettyPrinter.cs index b7e1bcb..891bf74 100644 --- a/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslPrettyPrinter.cs +++ b/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslPrettyPrinter.cs @@ -191,7 +191,9 @@ private static void PrintPass(StringBuilder sb, HlslPassNode pass, bool fuseTemp HashSet enabled = plan.States[index].Keywords.ToHashSet(StringComparer.Ordinal); string condition = string.Join(" && ", plan.Mask.Select(keyword => (enabled.Contains(keyword) ? "" : "!") + $"defined({keyword})")); - if (condition.Length == 0) condition = "1"; + if (condition.Length == 0) { + condition = "1"; + } sb.Append($" #{(index == 0 ? "if" : "elif")} {condition}\n"); PrintPassProgram(sb, pass.VariantPrograms[index], fuseTemps, stripRegisterBindings, false); } @@ -207,55 +209,41 @@ private static void PrintPass(StringBuilder sb, HlslPassNode pass, bool fuseTemp sb.Append(" }\n"); } + // Emits resource declarations and functions for one shader pass. private static void PrintPassProgram(StringBuilder sb, HlslPassNode pass, bool fuseTemps, bool stripRegisterBindings, bool entryPragmas) { - // Empty pass (no decompiled functions) — emit a valid stub so - // pass indices stay stable for Graphics.Blit(material, passIndex). - bool hasAnyFunction = pass.VertexFunction is not null || pass.FragmentFunction is not null - || pass.GeometryFunction is not null || pass.HullFunction is not null - || pass.DomainFunction is not null || pass.ComputeFunction is not null; - - if (!hasAnyFunction) - { - sb.AppendLine(" #pragma vertex vert"); - sb.AppendLine(" #pragma fragment frag"); - sb.AppendLine(" cbuffer _StubCB : register(b1) { float4x4 unity_MatrixVP; };"); - sb.AppendLine(" struct v2f { float4 pos : SV_POSITION; };"); - sb.AppendLine(" v2f vert(float4 v : POSITION) { v2f o; o.pos = mul(unity_MatrixVP, v); return o; }"); - sb.AppendLine(" float4 frag(v2f i) : SV_Target { return 0; }"); - } - - // Interpolator hand-off map: the vertex stage records which semantic - // fields carry named values (worldPos, worldNormal, ...) so the - // fragment stage can seed the matching input registers. + PrintEmptyPassStub(sb, pass); + // Carries semantic names from vertex outputs to fragment inputs. Dictionary stageInterpolators = new Dictionary(); - // Build cross-stage cbuffer variable rename mapping: when vert and - // frag have different variables at the same byte offset within a - // merged cbuffer, one variable is declared and the other's references - // in the function body are redirected. + // Shares one name for variables at the same constant-buffer offset. Dictionary cbufferRenames = BuildCbufferRenameMap(pass); - foreach (HlslResourceNode res in pass.Resources) - PrintResource(sb, res, stripRegisterBindings, cbufferRenames); - - foreach (HlslStructNode s in pass.Structs) - PrintStruct(sb, s); - + PrintPassDeclarations(sb, pass, stripRegisterBindings, cbufferRenames); if (pass.VertexFunction is not null) { - if (entryPragmas) sb.Append(" #pragma vertex vert\n"); + if (entryPragmas) + { + sb.Append(" #pragma vertex vert\n"); + } PrintFunction(sb, pass.VertexFunction, pass.Cbuffers, fuseTemps, stageOutputs: stageInterpolators, resources: pass.Resources, cbufferRenames: cbufferRenames); } if (pass.FragmentFunction is not null) { - if (entryPragmas) sb.Append(" #pragma fragment frag\n"); + if (entryPragmas) + { + sb.Append(" #pragma fragment frag\n"); + } PrintFunction(sb, pass.FragmentFunction, pass.Cbuffers, fuseTemps, stageInputs: stageInterpolators, resources: pass.Resources, cbufferRenames: cbufferRenames); } foreach (HlslFunctionNode? f in new[] { pass.GeometryFunction, pass.HullFunction, pass.DomainFunction, pass.ComputeFunction }) + { if (f is not null) + { PrintFunction(sb, f, pass.Cbuffers, fuseTemps, resources: pass.Resources, cbufferRenames: cbufferRenames); + } + } } @@ -2782,4 +2770,38 @@ private static string RenderTextureOp(IRExpression.TextureOperationExpression te $"/* TODO: no confirmed HLSL mapping for {tex.Operation} */ {resource}.{tex.Operation}({Args(sampler, coord)})", }; } + + // Emits a valid stub when a pass contains no decompiled functions. + private static void PrintEmptyPassStub(StringBuilder sb, HlslPassNode pass) + { + // Empty pass (no decompiled functions) — emit a valid stub so + // pass indices stay stable for Graphics.Blit(material, passIndex). + bool hasAnyFunction = pass.VertexFunction is not null || pass.FragmentFunction is not null + || pass.GeometryFunction is not null || pass.HullFunction is not null + || pass.DomainFunction is not null || pass.ComputeFunction is not null; + + if (!hasAnyFunction) + { + sb.AppendLine(" #pragma vertex vert"); + sb.AppendLine(" #pragma fragment frag"); + sb.AppendLine(" cbuffer _StubCB : register(b1) { float4x4 unity_MatrixVP; };"); + sb.AppendLine(" struct v2f { float4 pos : SV_POSITION; };"); + sb.AppendLine(" v2f vert(float4 v : POSITION) { v2f o; o.pos = mul(unity_MatrixVP, v); return o; }"); + sb.AppendLine(" float4 frag(v2f i) : SV_Target { return 0; }"); + } + } + + // Prints the resources and interpolator structures shared by a pass. + private static void PrintPassDeclarations(StringBuilder sb, HlslPassNode pass, bool stripRegisterBindings, Dictionary cbufferRenames) + { + foreach (HlslResourceNode res in pass.Resources) + { + PrintResource(sb, res, stripRegisterBindings, cbufferRenames); + } + + foreach (HlslStructNode s in pass.Structs) + { + PrintStruct(sb, s); + } + } } diff --git a/src/UnityShaderDecompiler.Core/DXBC/Metadata/ProgramVariantMetadata.cs b/src/UnityShaderDecompiler.Core/DXBC/Metadata/ProgramVariantMetadata.cs new file mode 100644 index 0000000..fa1b194 --- /dev/null +++ b/src/UnityShaderDecompiler.Core/DXBC/Metadata/ProgramVariantMetadata.cs @@ -0,0 +1,4 @@ +namespace UnityShaderDecompiler.Core.DXBC.Metadata; + +// Covers program variant metadata. +public sealed record ProgramVariantMetadata(int BlobIndex, string Stage, string[] Keywords, string[] KeywordMask); diff --git a/src/UnityShaderDecompiler.Core/DXBC/Metadata/Shadermetadata.cs b/src/UnityShaderDecompiler.Core/DXBC/Metadata/Shadermetadata.cs index 38a8c78..b281ded 100644 --- a/src/UnityShaderDecompiler.Core/DXBC/Metadata/Shadermetadata.cs +++ b/src/UnityShaderDecompiler.Core/DXBC/Metadata/Shadermetadata.cs @@ -45,8 +45,12 @@ public static ShaderMetadata Load(string path) } if (root.TryGetProperty("keywordFlags", out JsonElement keywordFlags)) + { foreach (JsonProperty keyword in keywordFlags.EnumerateObject()) + { meta.KeywordFlags.Add(keyword.Name, keyword.Value.GetInt32()); + } + } if (root.TryGetProperty("properties", out var props) && props.ValueKind == JsonValueKind.Array) { @@ -196,18 +200,22 @@ public static PassMetadata From(JsonElement el) result.ConstantBuffers.Add(CbufferMetadata.From(cb)); if (el.TryGetProperty("directXPrograms", out JsonElement programs)) + { foreach (JsonElement program in programs.EnumerateArray()) + { result.DirectXPrograms.Add(new ProgramVariantMetadata( program.GetProperty("blobIndex").GetInt32(), program.GetProperty("stage").GetString()!, program.GetProperty("keywords").EnumerateArray().Select(k => k.GetString()!).ToArray(), program.GetProperty("keywordMask").EnumerateArray().Select(k => k.GetString()!).ToArray())); + } + } return result; } } -public sealed record ProgramVariantMetadata(int BlobIndex, string Stage, string[] Keywords, string[] KeywordMask); + /// One cbuffer in a pass: register slot -> name + variables. public sealed class CbufferMetadata diff --git a/src/UnityShaderDecompiler.Core/Decompiler.cs b/src/UnityShaderDecompiler.Core/Decompiler.cs index 76942b3..d3ff6b4 100644 --- a/src/UnityShaderDecompiler.Core/Decompiler.cs +++ b/src/UnityShaderDecompiler.Core/Decompiler.cs @@ -358,9 +358,13 @@ public static DecompilationResult Decompile( // serialization order and emit one pass per distinct compiled // (vertex, fragment) variant pair. if (options.RestoreKeywordVariants) + { BuildRuntimeVariantPasses(astShader, allPasses, candidates, project.Metadata.KeywordFlags, log); + } else + { BuildVariantPasses(astShader, allPasses, candidates, log, failedPrograms); + } log.WriteLine(); log.WriteLine("HLSL AST (Stage 2)"); @@ -399,7 +403,9 @@ public static DecompilationResult Decompile( { outputPath = Path.Combine(outRoot, SafeFileName(astShader.Name) + ".shader"); if (options.RestoreKeywordVariants) + { RuntimeVariantManifest.Write(outputPath, text, astShader, candidates); + } File.WriteAllText(outputPath, text); log.WriteLine(); log.WriteLine($"Stage 13/14: full shader written to {outputPath} ({text.Length} chars)"); @@ -605,6 +611,7 @@ private static void BuildVariantPasses( log.WriteLine($"BuildVariantPasses: {groups.Count} pass group(s), {totalPasses} pass(es) emitted total."); } + // Expands serialized stage combinations while preserving failure policy. private static void BuildSerializedVariantPasses( HlslShaderNode shader, List shells, List candidates, Dictionary textureNames, TextWriter log, ISet failedPrograms) @@ -613,43 +620,19 @@ private static void BuildSerializedVariantPasses( HashSet covered = new HashSet(); foreach (HlslPassNode shell in shells) { - if (shell.DirectXPrograms.Count == 0) continue; - List groups = VariantPairing.Group(shell.DirectXPrograms); - HlslSubShaderNode parent = shader.SubShaders.Single(ss => ss.Passes.Contains(shell)); - int insertAt = parent.Passes.IndexOf(shell); - foreach (ProgramVariantMetadata[] group in groups) + if (shell.DirectXPrograms.Count == 0) { - if (group.Any(reference => failedPrograms.Contains(reference.BlobIndex))) - { - // Preserve the caller's existing log-and-continue failure policy. - covered.UnionWith(group.Select(reference => reference.BlobIndex)); - log.WriteLine("Skipped serialized variant after a logged subprogram error."); - continue; - } - HlslPassNode pass = insertAt == parent.Passes.IndexOf(shell) ? shell : ClonePass(shell); - if (!ReferenceEquals(pass, shell)) parent.Passes.Insert(insertAt, pass); - DecompiledSubprogram? previous = null; - foreach (ProgramVariantMetadata reference in group) - { - if (!byIndex.TryGetValue(reference.BlobIndex, out DecompiledSubprogram? program)) - throw new InvalidDataException("Serialized variant references a missing DXBC program."); - if (program.Stage.ToString() != reference.Stage) - throw new InvalidDataException("Serialized variant stage does not match its DXBC program."); - if (previous is not null && (previous.Dxbc.OutputSignature is null || program.Dxbc.InputSignature is null - || !VariantPairing.InterfacesMatch(previous.Dxbc.OutputSignature.Elements, program.Dxbc.InputSignature.Elements))) - throw new InvalidDataException($"Variant {previous.Index}/{program.Index} has incompatible interpolators."); - AttachPassProgram(pass, program, textureNames, log); - covered.Add(program.Index); - previous = program; - } - insertAt++; + continue; } - log.WriteLine($"Serialized variants: {groups.Count} stage combinations."); + BuildSerializedPassGroups(shader, shell, byIndex, textureNames, log, failedPrograms, covered); } if (byIndex.Keys.Except(covered).Any()) + { throw new InvalidDataException("DXBC programs are missing from serialized variant coverage."); + } } + // Builds keyword-selected variants for each serialized pass. private static void BuildRuntimeVariantPasses(HlslShaderNode shader, List shells, List candidates, Dictionary keywordFlags, TextWriter log) { @@ -658,42 +641,30 @@ private static void BuildRuntimeVariantPasses(HlslShaderNode shader, List textureNames = new Dictionary(); uint textureSlot = 0; foreach (HlslPropertyNode property in shader.Properties) + { if (property.Kind == HlslPropertyKind.Texture) + { textureNames[textureSlot++] = property.Name; + } + } foreach (HlslPassNode shell in shells) { if (shell.DirectXPrograms.Count == 0) + { throw new InvalidDataException("Runtime keyword reconstruction requires serialized program references for every pass."); + } RuntimeVariantPlan plan = RuntimeVariantPlan.Build(shell.DirectXPrograms, keywordFlags); shell.RuntimeVariants = plan; foreach (RuntimeVariantState state in plan.States) { - if (!byIndex.TryGetValue(state.Vertex, out DecompiledSubprogram? vertex) - || !byIndex.TryGetValue(state.Fragment, out DecompiledSubprogram? fragment)) - throw new InvalidDataException("Serialized variant references a missing DXBC program."); - if (vertex.Stage != HlslShaderStage.Vertex || fragment.Stage != HlslShaderStage.Fragment) - throw new InvalidDataException("Serialized variant stage does not match its DXBC program."); - if (vertex.Dxbc.OutputSignature is null || fragment.Dxbc.InputSignature is null - || !VariantPairing.InterfacesMatch(vertex.Dxbc.OutputSignature.Elements, fragment.Dxbc.InputSignature.Elements)) - throw new InvalidDataException($"Variant {state.Vertex}/{state.Fragment} has incompatible interpolators."); - uint major = Math.Max(vertex.Dxbc.Shader!.MajorVersion, fragment.Dxbc.Shader!.MajorVersion); - if (major is not (4 or 5)) - throw new InvalidDataException("Runtime keyword reconstruction requires shader model 4 or 5."); - string target = major == 5 ? "5.0" : "4.0"; - if (shell.VariantPrograms.Count != 0 && shell.ShaderTarget != target) - throw new InvalidDataException("Runtime variants with different shader targets are not supported."); - shell.ShaderTarget = target; - HlslPassNode leaf = ClonePass(shell); - AttachPassProgram(leaf, vertex, textureNames, log); - AttachPassProgram(leaf, fragment, textureNames, log); - shell.VariantPrograms.Add(leaf); - covered.Add(state.Vertex); - covered.Add(state.Fragment); + AttachRuntimeState(shell, state, byIndex, textureNames, log, covered); } log.WriteLine($"Runtime variants: {plan.States.Count} keyword states in one serialized pass."); } if (byIndex.Keys.Except(covered).Any()) + { throw new InvalidDataException("DXBC programs are missing from serialized variant coverage."); + } } // Attaches one candidate's function, resources and structs to a pass, @@ -858,4 +829,97 @@ public static string ProjectOutputRoot() return FindOutputRoot(); } + + // Expands the compatible stage combinations for one serialized pass. + private static void BuildSerializedPassGroups(HlslShaderNode shader, HlslPassNode shell, Dictionary byIndex, Dictionary textureNames, TextWriter log, ISet failedPrograms, HashSet covered) + { + List groups = VariantPairing.Group(shell.DirectXPrograms); + HlslSubShaderNode parent = shader.SubShaders.Single(ss => ss.Passes.Contains(shell)); + int insertAt = parent.Passes.IndexOf(shell); + foreach (ProgramVariantMetadata[] group in groups) + { + if (group.Any(reference => failedPrograms.Contains(reference.BlobIndex))) + { + // Preserve the caller's existing log-and-continue failure policy. + covered.UnionWith(group.Select(reference => reference.BlobIndex)); + log.WriteLine("Skipped serialized variant after a logged subprogram error."); + continue; + } + HlslPassNode pass = insertAt == parent.Passes.IndexOf(shell) ? shell : ClonePass(shell); + if (!ReferenceEquals(pass, shell)) + { + parent.Passes.Insert(insertAt, pass); + } + AttachSerializedGroup(pass, group, byIndex, textureNames, log, covered); + insertAt++; + } + log.WriteLine($"Serialized variants: {groups.Count} stage combinations."); + } + + // Validates adjacent stage interfaces and attaches a complete serialized group. + private static void AttachSerializedGroup(HlslPassNode pass, ProgramVariantMetadata[] group, Dictionary byIndex, Dictionary textureNames, TextWriter log, HashSet covered) + { + DecompiledSubprogram? previous = null; + foreach (ProgramVariantMetadata reference in group) + { + if (!byIndex.TryGetValue(reference.BlobIndex, out DecompiledSubprogram? program)) + { + throw new InvalidDataException("Serialized variant references a missing DXBC program."); + } + if (program.Stage.ToString() != reference.Stage) + { + throw new InvalidDataException("Serialized variant stage does not match its DXBC program."); + } + if (previous is not null && (previous.Dxbc.OutputSignature is null || program.Dxbc.InputSignature is null + || !VariantPairing.InterfacesMatch(previous.Dxbc.OutputSignature.Elements, program.Dxbc.InputSignature.Elements))) + { + throw new InvalidDataException($"Variant {previous.Index}/{program.Index} has incompatible interpolators."); + } + AttachPassProgram(pass, program, textureNames, log); + covered.Add(program.Index); + previous = program; + } + } + + // Attaches both programs of one runtime keyword state. + private static void AttachRuntimeState(HlslPassNode shell, RuntimeVariantState state, Dictionary byIndex, Dictionary textureNames, TextWriter log, HashSet covered) + { + if (!byIndex.TryGetValue(state.Vertex, out DecompiledSubprogram? vertex) + || !byIndex.TryGetValue(state.Fragment, out DecompiledSubprogram? fragment)) + { + throw new InvalidDataException("Serialized variant references a missing DXBC program."); + } + if (vertex.Stage != HlslShaderStage.Vertex || fragment.Stage != HlslShaderStage.Fragment) + { + throw new InvalidDataException("Serialized variant stage does not match its DXBC program."); + } + if (vertex.Dxbc.OutputSignature is null || fragment.Dxbc.InputSignature is null + || !VariantPairing.InterfacesMatch(vertex.Dxbc.OutputSignature.Elements, fragment.Dxbc.InputSignature.Elements)) + { + throw new InvalidDataException($"Variant {state.Vertex}/{state.Fragment} has incompatible interpolators."); + } + SetRuntimeShaderTarget(shell, vertex, fragment); + HlslPassNode leaf = ClonePass(shell); + AttachPassProgram(leaf, vertex, textureNames, log); + AttachPassProgram(leaf, fragment, textureNames, log); + shell.VariantPrograms.Add(leaf); + covered.Add(state.Vertex); + covered.Add(state.Fragment); + } + + // Requires a common supported shader target across runtime variants. + private static void SetRuntimeShaderTarget(HlslPassNode shell, DecompiledSubprogram vertex, DecompiledSubprogram fragment) + { + uint major = Math.Max(vertex.Dxbc.Shader!.MajorVersion, fragment.Dxbc.Shader!.MajorVersion); + if (major is not (4 or 5)) + { + throw new InvalidDataException("Runtime keyword reconstruction requires shader model 4 or 5."); + } + string target = major == 5 ? "5.0" : "4.0"; + if (shell.VariantPrograms.Count != 0 && shell.ShaderTarget != target) + { + throw new InvalidDataException("Runtime variants with different shader targets are not supported."); + } + shell.ShaderTarget = target; + } } diff --git a/src/UnityShaderDecompiler.Core/RuntimeCompilationCase.cs b/src/UnityShaderDecompiler.Core/RuntimeCompilationCase.cs new file mode 100644 index 0000000..83d1a83 --- /dev/null +++ b/src/UnityShaderDecompiler.Core/RuntimeCompilationCase.cs @@ -0,0 +1,5 @@ +namespace UnityShaderDecompiler.Core; + +// Describes one vertex/fragment combination for native compilation. +internal sealed record RuntimeCompilationCase(int Pass, int Variant, string Hlsl, + int Vertex, int Fragment, string VertexProfile, string FragmentProfile); diff --git a/src/UnityShaderDecompiler.Core/RuntimeKeywordGroup.cs b/src/UnityShaderDecompiler.Core/RuntimeKeywordGroup.cs new file mode 100644 index 0000000..a8423a3 --- /dev/null +++ b/src/UnityShaderDecompiler.Core/RuntimeKeywordGroup.cs @@ -0,0 +1,4 @@ +namespace UnityShaderDecompiler.Core; + +// Describes an independent Unity keyword set and its scope. +internal sealed record RuntimeKeywordGroup(string[] Keywords, bool AllowsDisabled, bool Local); diff --git a/src/UnityShaderDecompiler.Core/RuntimeVariantManifest.cs b/src/UnityShaderDecompiler.Core/RuntimeVariantManifest.cs index 907921a..21920f1 100644 --- a/src/UnityShaderDecompiler.Core/RuntimeVariantManifest.cs +++ b/src/UnityShaderDecompiler.Core/RuntimeVariantManifest.cs @@ -6,13 +6,12 @@ namespace UnityShaderDecompiler.Core; -internal sealed record RuntimeCompilationCase(int Pass, int Variant, string Hlsl, - int Vertex, int Fragment, string VertexProfile, string FragmentProfile); - +// Reads and writes hash-bound compilation manifests. internal static class RuntimeVariantManifest { internal const string Marker = "#error No serialized program matches this shader keyword state"; + // Writes a hash-bound manifest for every emitted keyword variant. internal static void Write(string shaderPath, string text, HlslShaderNode shader, IReadOnlyList programs) { @@ -26,7 +25,9 @@ internal static void Write(string shaderPath, string text, HlslShaderNode shader passIndex = index, variants = pass.RuntimeVariants!.States.Select(state => new { - vertex = state.Vertex, fragment = state.Fragment, keywords = state.Keywords, + vertex = state.Vertex, + fragment = state.Fragment, + keywords = state.Keywords, vertexProfile = byIndex[state.Vertex].Dxbc.Shader!.ShaderModel, fragmentProfile = byIndex[state.Fragment].Dxbc.Shader!.ShaderModel, }), @@ -36,6 +37,7 @@ internal static void Write(string shaderPath, string text, HlslShaderNode shader JsonSerializer.Serialize(manifest, new JsonSerializerOptions { WriteIndented = true })); } + // Reads and validates the requested serialized data. internal static List Read(string shaderPath) { string text = File.ReadAllText(shaderPath); @@ -44,37 +46,30 @@ internal static List Read(string shaderPath) string hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(text))); if (root.GetProperty("schemaVersion").GetInt32() != 1 || !string.Equals(root.GetProperty("shaderSha256").GetString(), hash, StringComparison.OrdinalIgnoreCase)) + { throw new InvalidDataException("Variant manifest does not match the generated shader."); + } MatchCollection blocks = Regex.Matches(text, @"HLSLPROGRAM(.*?)ENDHLSL", RegexOptions.Singleline); JsonElement passes = root.GetProperty("passes"); if (passes.GetArrayLength() != blocks.Count || blocks.Count == 0) + { throw new InvalidDataException("Variant manifest has an invalid pass count."); + } List cases = new List(); int passIndex = 0; foreach (JsonElement pass in passes.EnumerateArray()) { if (pass.GetProperty("passIndex").GetInt32() != passIndex) - throw new InvalidDataException("Variant manifest pass order is invalid."); - int variantIndex = 0; - foreach (JsonElement variant in pass.GetProperty("variants").EnumerateArray()) { - string[] keywords = variant.GetProperty("keywords").EnumerateArray().Select(value => value.GetString()!).ToArray(); - if (keywords.Any(keyword => !Regex.IsMatch(keyword, @"\A[A-Za-z_][A-Za-z0-9_]*\z"))) - throw new InvalidDataException("Variant manifest contains an invalid keyword."); - string vertexProfile = variant.GetProperty("vertexProfile").GetString()!; - string fragmentProfile = variant.GetProperty("fragmentProfile").GetString()!; - if (!Regex.IsMatch(vertexProfile, @"\Avs_[45]_0\z") || !Regex.IsMatch(fragmentProfile, @"\Aps_[45]_0\z")) - throw new InvalidDataException("Variant manifest contains an unsupported shader profile."); - string definitions = string.Concat(keywords.Select(keyword => $"#define {keyword} 1\n")); - cases.Add(new RuntimeCompilationCase(passIndex, variantIndex++, definitions + blocks[passIndex].Groups[1].Value, - variant.GetProperty("vertex").GetInt32(), variant.GetProperty("fragment").GetInt32(), vertexProfile, fragmentProfile)); + throw new InvalidDataException("Variant manifest pass order is invalid."); } - if (variantIndex == 0) throw new InvalidDataException("Variant manifest contains an empty pass."); + ReadPassCases(pass, passIndex, blocks, cases); passIndex++; } return cases; } + // Compiles every vertex and fragment combination from the manifest. internal static void VerifyCompilation(string shaderPath) { List cases = Read(shaderPath); @@ -86,4 +81,31 @@ internal static void VerifyCompilation(string shaderPath) } Console.WriteLine($"passes={cases.Select(item => item.Pass).Distinct().Count()} variants={cases.Count} compiled={cases.Count} compileFailures=0"); } + + // Validates and expands the compilation cases of one serialized pass. + private static void ReadPassCases(JsonElement pass, int passIndex, MatchCollection blocks, List cases) + { + int variantIndex = 0; + foreach (JsonElement variant in pass.GetProperty("variants").EnumerateArray()) + { + string[] keywords = variant.GetProperty("keywords").EnumerateArray().Select(value => value.GetString()!).ToArray(); + if (keywords.Any(keyword => !Regex.IsMatch(keyword, @"\A[A-Za-z_][A-Za-z0-9_]*\z"))) + { + throw new InvalidDataException("Variant manifest contains an invalid keyword."); + } + string vertexProfile = variant.GetProperty("vertexProfile").GetString()!; + string fragmentProfile = variant.GetProperty("fragmentProfile").GetString()!; + if (!Regex.IsMatch(vertexProfile, @"\Avs_[45]_0\z") || !Regex.IsMatch(fragmentProfile, @"\Aps_[45]_0\z")) + { + throw new InvalidDataException("Variant manifest contains an unsupported shader profile."); + } + string definitions = string.Concat(keywords.Select(keyword => $"#define {keyword} 1\n")); + cases.Add(new RuntimeCompilationCase(passIndex, variantIndex++, definitions + blocks[passIndex].Groups[1].Value, + variant.GetProperty("vertex").GetInt32(), variant.GetProperty("fragment").GetInt32(), vertexProfile, fragmentProfile)); + } + if (variantIndex == 0) + { + throw new InvalidDataException("Variant manifest contains an empty pass."); + } + } } diff --git a/src/UnityShaderDecompiler.Core/RuntimeVariantPlan.cs b/src/UnityShaderDecompiler.Core/RuntimeVariantPlan.cs index cb82047..b352722 100644 --- a/src/UnityShaderDecompiler.Core/RuntimeVariantPlan.cs +++ b/src/UnityShaderDecompiler.Core/RuntimeVariantPlan.cs @@ -3,9 +3,7 @@ namespace UnityShaderDecompiler.Core; -internal sealed record RuntimeVariantState(int Vertex, int Fragment, string[] Keywords); -internal sealed record RuntimeKeywordGroup(string[] Keywords, bool AllowsDisabled, bool Local); - +// Reconstructs exact Unity keyword groups from serialized program states. internal sealed class RuntimeVariantPlan { internal required string[] Mask { get; init; } @@ -14,71 +12,122 @@ internal sealed class RuntimeVariantPlan internal required string[] StrippedKeywords { get; init; } internal required HashSet LocalKeywords { get; init; } + // Reconstructs complete keyword groups from serialized program states. internal static RuntimeVariantPlan Build(IReadOnlyList programs, IReadOnlyDictionary keywordFlags) { VariantPairing.Pair(programs); string[] mask = programs.SelectMany(p => p.KeywordMask).Distinct(StringComparer.Ordinal) .Order(StringComparer.Ordinal).ToArray(); + ValidateKeywords(mask, keywordFlags); + Dictionary states = BuildStates(programs); + HashSet[] enabledStates = states.Values.Select(s => s.Keywords.ToHashSet(StringComparer.Ordinal)).ToArray(); + string[] active = enabledStates.SelectMany(s => s).Distinct(StringComparer.Ordinal).Order(StringComparer.Ordinal).ToArray(); + List groups = BuildGroups(active, enabledStates, states.Count, keywordFlags); + return new RuntimeVariantPlan + { + Mask = mask, + States = states.OrderBy(p => p.Key, StringComparer.Ordinal).Select(p => p.Value).ToList(), + Groups = groups, + StrippedKeywords = mask.Except(active, StringComparer.Ordinal).ToArray(), + LocalKeywords = mask.Where(keyword => keywordFlags[keyword] == 1).ToHashSet(StringComparer.Ordinal), + }; + } + + // Rejects invalid keyword identifiers and missing scope flags. + private static void ValidateKeywords(string[] mask, IReadOnlyDictionary keywordFlags) + { foreach (string keyword in mask) { if (!Regex.IsMatch(keyword, @"\A[A-Za-z_][A-Za-z0-9_]*\z")) + { throw new InvalidDataException($"Invalid shader keyword '{keyword}'."); + } if (!keywordFlags.TryGetValue(keyword, out int flags) || flags is not (0 or 1)) + { throw new InvalidDataException($"Missing or unsupported serialized scope flags for keyword '{keyword}'."); + } } + } + // Pairs compatible stages and rejects ambiguous keyword states. + private static Dictionary BuildStates(IReadOnlyList programs) + { Dictionary states = new Dictionary(StringComparer.Ordinal); foreach (ProgramVariantMetadata vertex in programs.Where(p => p.Stage == "Vertex")) + { foreach (ProgramVariantMetadata fragment in programs.Where(p => p.Stage == "Fragment")) { HashSet shared = vertex.KeywordMask.Intersect(fragment.KeywordMask).ToHashSet(StringComparer.Ordinal); if (!vertex.Keywords.Where(shared.Contains).ToHashSet(StringComparer.Ordinal) - .SetEquals(fragment.Keywords.Where(shared.Contains))) continue; + .SetEquals(fragment.Keywords.Where(shared.Contains))) + { + continue; + } string[] enabled = vertex.Keywords.Union(fragment.Keywords, StringComparer.Ordinal) .Order(StringComparer.Ordinal).ToArray(); string key = string.Join(";", enabled); RuntimeVariantState state = new RuntimeVariantState(vertex.BlobIndex, fragment.BlobIndex, enabled); if (states.TryGetValue(key, out RuntimeVariantState? previous) && (previous.Vertex != state.Vertex || previous.Fragment != state.Fragment)) + { throw new InvalidDataException($"Ambiguous shader programs for keyword state '{key}'."); + } states[key] = state; } + } + return states; + } - HashSet[] enabledStates = states.Values.Select(s => s.Keywords.ToHashSet(StringComparer.Ordinal)).ToArray(); - string[] active = enabledStates.SelectMany(s => s).Distinct(StringComparer.Ordinal).Order(StringComparer.Ordinal).ToArray(); + // Builds independent keyword groups and checks their Cartesian product. + private static List BuildGroups(string[] active, HashSet[] enabledStates, int stateCount, IReadOnlyDictionary keywordFlags) + { HashSet remaining = active.ToHashSet(StringComparer.Ordinal); List groups = new List(); long combinations = 1; while (remaining.Count != 0) { - HashSet group = new HashSet(StringComparer.Ordinal) { remaining.Order(StringComparer.Ordinal).First() }; - Queue pending = new Queue(group); - while (pending.TryDequeue(out string? keyword)) - foreach (string other in active) - if (!enabledStates.Any(state => state.Contains(keyword) && state.Contains(other)) && group.Add(other)) - pending.Enqueue(other); + HashSet group = FindExclusiveGroup(remaining, active, enabledStates); if (enabledStates.Any(state => state.Count(group.Contains) > 1)) + { throw new InvalidDataException("Serialized keyword states cannot be represented by independent Unity keyword sets."); + } bool allowsDisabled = enabledStates.Any(state => !state.Overlaps(group)); combinations = checked(combinations * (group.Count + (allowsDisabled ? 1 : 0))); - if (combinations > states.Count) + if (combinations > stateCount) + { throw new InvalidDataException("Serialized variants are incomplete for the reconstructed Unity keyword sets."); + } if (group.Select(keyword => keywordFlags[keyword]).Distinct().Count() != 1) + { throw new InvalidDataException("An exclusive keyword set mixes local and global scope."); + } groups.Add(new RuntimeKeywordGroup(group.Order(StringComparer.Ordinal).ToArray(), allowsDisabled, keywordFlags[group.First()] == 1)); remaining.ExceptWith(group); } // Disjoint exclusive sets plus equal cardinality prove the exact Cartesian product. - if (combinations != states.Count) + if (combinations != stateCount) + { throw new InvalidDataException("Serialized variants do not form complete Unity keyword sets."); - return new RuntimeVariantPlan + } + return groups; + } + + // Finds keywords that cannot be enabled together. + private static HashSet FindExclusiveGroup(HashSet remaining, string[] active, HashSet[] enabledStates) + { + HashSet group = new HashSet(StringComparer.Ordinal) { remaining.Order(StringComparer.Ordinal).First() }; + Queue pending = new Queue(group); + while (pending.TryDequeue(out string? keyword)) { - Mask = mask, - States = states.OrderBy(p => p.Key, StringComparer.Ordinal).Select(p => p.Value).ToList(), - Groups = groups, - StrippedKeywords = mask.Except(active, StringComparer.Ordinal).ToArray(), - LocalKeywords = mask.Where(keyword => keywordFlags[keyword] == 1).ToHashSet(StringComparer.Ordinal), - }; + foreach (string other in active) + { + if (!enabledStates.Any(state => state.Contains(keyword) && state.Contains(other)) && group.Add(other)) + { + pending.Enqueue(other); + } + } + } + return group; } } diff --git a/src/UnityShaderDecompiler.Core/RuntimeVariantState.cs b/src/UnityShaderDecompiler.Core/RuntimeVariantState.cs new file mode 100644 index 0000000..1a272a6 --- /dev/null +++ b/src/UnityShaderDecompiler.Core/RuntimeVariantState.cs @@ -0,0 +1,4 @@ +namespace UnityShaderDecompiler.Core; + +// Associates an enabled keyword state with its vertex and fragment programs. +internal sealed record RuntimeVariantState(int Vertex, int Fragment, string[] Keywords); diff --git a/src/UnityShaderDecompiler.Core/VariantPairing.cs b/src/UnityShaderDecompiler.Core/VariantPairing.cs index 420d0d8..b85d6d7 100644 --- a/src/UnityShaderDecompiler.Core/VariantPairing.cs +++ b/src/UnityShaderDecompiler.Core/VariantPairing.cs @@ -3,6 +3,7 @@ namespace UnityShaderDecompiler.Core; +// Matches serialized graphics stages without inventing keyword combinations. internal static class VariantPairing { // Expanded passes may contain any serialized graphics stage, not only vertex/fragment. @@ -10,42 +11,41 @@ internal static List Group(IReadOnlyList !stageOrder.Contains(program.Stage))) + { throw new InvalidDataException("Serialized variants contain an unknown graphics stage."); + } List groups = new List { Array.Empty() }; foreach (string stageName in stageOrder) { ProgramVariantMetadata[] stage = programs.Where(program => program.Stage == stageName).ToArray(); - if (stage.Length == 0) continue; - foreach (ProgramVariantMetadata program in stage) + if (stage.Length == 0) { - if (program.BlobIndex < 0 || program.Keywords.Any(string.IsNullOrWhiteSpace) - || program.KeywordMask.Any(string.IsNullOrWhiteSpace) - || program.Keywords.Distinct(StringComparer.Ordinal).Count() != program.Keywords.Length - || program.KeywordMask.Distinct(StringComparer.Ordinal).Count() != program.KeywordMask.Length - || program.Keywords.Except(program.KeywordMask).Any() - || !program.KeywordMask.ToHashSet(StringComparer.Ordinal).SetEquals(stage[0].KeywordMask)) - throw new InvalidDataException("Serialized variants contain invalid or inconsistent stage metadata."); + continue; } + ValidateStage(stage); List next = new List(); foreach (ProgramVariantMetadata[] group in groups) + { foreach (ProgramVariantMetadata candidate in stage) { - bool compatible = group.All(previous => + bool compatible = group.All(previous => StatesMatch(previous, candidate)); + if (compatible) { - HashSet shared = previous.KeywordMask.Intersect(candidate.KeywordMask).ToHashSet(StringComparer.Ordinal); - return previous.Keywords.Where(shared.Contains).ToHashSet(StringComparer.Ordinal) - .SetEquals(candidate.Keywords.Where(shared.Contains)); - }); - if (compatible) next.Add(group.Append(candidate).ToArray()); + next.Add(group.Append(candidate).ToArray()); + } } + } groups = next; } HashSet matched = groups.SelectMany(group => group).ToHashSet(); if (programs.Any(program => !matched.Contains(program))) + { throw new InvalidDataException("Serialized shader variants contain an unmatched stage."); + } return groups.DistinctBy(group => string.Join(",", group.Select(program => program.BlobIndex))).ToList(); } + // Checks register locations, component types and masks between stages. internal static bool InterfacesMatch(IReadOnlyList outputs, IReadOnlyList inputs) { // Adjacent DirectX stages share register locations as well as semantics. @@ -56,30 +56,22 @@ internal static bool InterfacesMatch(IReadOnlyList outputs, IR && output.SemanticIndex == input.SemanticIndex && output.ComponentType == input.ComponentType && output.Register == input.Register) .Aggregate(0, (mask, output) => mask | output.Mask); - if ((available & input.Mask) != input.Mask) return false; + if ((available & input.Mask) != input.Mask) + { + return false; + } } return true; } + // Matches vertex and fragment programs by their shared keyword states. internal static List<(int Vertex, int Fragment)> Pair(IReadOnlyList programs) { - if (programs.Any(p => p.BlobIndex < 0 || p.Keywords.Any(string.IsNullOrWhiteSpace) - || p.KeywordMask.Any(string.IsNullOrWhiteSpace) - || p.Keywords.Distinct(StringComparer.Ordinal).Count() != p.Keywords.Length - || p.KeywordMask.Distinct(StringComparer.Ordinal).Count() != p.KeywordMask.Length)) - throw new InvalidDataException("Shader variants contain invalid indices or duplicate/empty keywords."); - if (programs.Any(p => p.Stage is not ("Vertex" or "Fragment"))) - throw new InvalidDataException("Serialized variant pairing currently supports vertex/fragment stages only."); - ProgramVariantMetadata[] vertices = programs.Where(p => p.Stage == "Vertex").ToArray(); - ProgramVariantMetadata[] fragments = programs.Where(p => p.Stage == "Fragment").ToArray(); - if (programs.Any(p => p.Keywords.Except(p.KeywordMask).Any())) - throw new InvalidDataException("A shader variant enables a keyword outside its stage mask."); - foreach (ProgramVariantMetadata[] stage in new[] { vertices, fragments }) - if (stage.Any(p => !p.KeywordMask.ToHashSet(StringComparer.Ordinal).SetEquals(stage[0].KeywordMask))) - throw new InvalidDataException("Shader variants have inconsistent keyword masks within a stage."); + (ProgramVariantMetadata[] vertices, ProgramVariantMetadata[] fragments) = ValidatePairing(programs); List<(int Vertex, int Fragment)> pairs = new List<(int Vertex, int Fragment)>(); HashSet matched = new HashSet(); foreach (ProgramVariantMetadata vertex in vertices) + { foreach (ProgramVariantMetadata fragment in fragments) { HashSet shared = vertex.KeywordMask.Intersect(fragment.KeywordMask).ToHashSet(); @@ -90,9 +82,66 @@ internal static bool InterfacesMatch(IReadOnlyList outputs, IR matched.Add(fragment); } } + } // A fragment-only keyword can reuse a vertex program; pairing is not 1:1. if (programs.Any(program => !matched.Contains(program)) || pairs.Count == 0) + { throw new InvalidDataException("Serialized shader variants contain an unmatched stage."); + } return pairs.Distinct().ToList(); } + + // Rejects invalid indices and inconsistent keyword masks within a stage. + private static void ValidateStage(ProgramVariantMetadata[] stage) + { + foreach (ProgramVariantMetadata program in stage) + { + if (program.BlobIndex < 0 || program.Keywords.Any(string.IsNullOrWhiteSpace) + || program.KeywordMask.Any(string.IsNullOrWhiteSpace) + || program.Keywords.Distinct(StringComparer.Ordinal).Count() != program.Keywords.Length + || program.KeywordMask.Distinct(StringComparer.Ordinal).Count() != program.KeywordMask.Length + || program.Keywords.Except(program.KeywordMask).Any() + || !program.KeywordMask.ToHashSet(StringComparer.Ordinal).SetEquals(stage[0].KeywordMask)) + { + throw new InvalidDataException("Serialized variants contain invalid or inconsistent stage metadata."); + } + } + } + + // Validates vertex and fragment metadata before pairing. + private static (ProgramVariantMetadata[] Vertices, ProgramVariantMetadata[] Fragments) ValidatePairing(IReadOnlyList programs) + { + if (programs.Any(p => p.BlobIndex < 0 || p.Keywords.Any(string.IsNullOrWhiteSpace) + || p.KeywordMask.Any(string.IsNullOrWhiteSpace) + || p.Keywords.Distinct(StringComparer.Ordinal).Count() != p.Keywords.Length + || p.KeywordMask.Distinct(StringComparer.Ordinal).Count() != p.KeywordMask.Length)) + { + throw new InvalidDataException("Shader variants contain invalid indices or duplicate/empty keywords."); + } + if (programs.Any(p => p.Stage is not ("Vertex" or "Fragment"))) + { + throw new InvalidDataException("Serialized variant pairing currently supports vertex/fragment stages only."); + } + ProgramVariantMetadata[] vertices = programs.Where(p => p.Stage == "Vertex").ToArray(); + ProgramVariantMetadata[] fragments = programs.Where(p => p.Stage == "Fragment").ToArray(); + if (programs.Any(p => p.Keywords.Except(p.KeywordMask).Any())) + { + throw new InvalidDataException("A shader variant enables a keyword outside its stage mask."); + } + foreach (ProgramVariantMetadata[] stage in new[] { vertices, fragments }) + { + if (stage.Any(p => !p.KeywordMask.ToHashSet(StringComparer.Ordinal).SetEquals(stage[0].KeywordMask))) + { + throw new InvalidDataException("Shader variants have inconsistent keyword masks within a stage."); + } + } + return (vertices, fragments); + } + // Compares the enabled state of keywords shared by adjacent stages. + internal static bool StatesMatch(ProgramVariantMetadata previous, ProgramVariantMetadata candidate) + { + HashSet shared = previous.KeywordMask.Intersect(candidate.KeywordMask).ToHashSet(StringComparer.Ordinal); + return previous.Keywords.Where(shared.Contains).ToHashSet(StringComparer.Ordinal) + .SetEquals(candidate.Keywords.Where(shared.Contains)); + } } diff --git a/tests/UnityRuntime/README.md b/tests/UnityRuntime/README.md index 53e9956..98007fd 100644 --- a/tests/UnityRuntime/README.md +++ b/tests/UnityRuntime/README.md @@ -8,12 +8,12 @@ are needed. Use Windows, Direct3D 11 and Unity **6000.0.75f1** (Built-in pipelin With `UNITY_CLI` pointing to the Unity CLI executable and `TEST_PROJECT` to a disposable project created with that Editor version: -```powershell -$env:SHADER_RUNTIME_TEST_OUTPUT = "$env:TEST_PROJECT/Assets" +```bash +export SHADER_RUNTIME_TEST_OUTPUT="$TEST_PROJECT/Assets" dotnet test UnityShaderDecompiler.sln -c Release --filter FullyQualifiedName~RuntimeVariantEmissionTests -New-Item -ItemType Directory -Force "$env:TEST_PROJECT/Assets/Editor" | Out-Null -Copy-Item tests/UnityRuntime/RuntimeKeywordChecks.cs "$env:TEST_PROJECT/Assets/Editor/" -& $env:UNITY_CLI run $env:TEST_PROJECT --editor-version 6000.0.75f1 --timeout 180 --format json -- -executeMethod RuntimeKeywordChecks.Run -force-d3d11 +mkdir -p "$TEST_PROJECT/Assets/Editor" +cp tests/UnityRuntime/*.cs "$TEST_PROJECT/Assets/Editor/" +"$UNITY_CLI" run "$TEST_PROJECT" --editor-version 6000.0.75f1 --timeout 180 --format json -- -executeMethod RuntimeKeywordChecks.Run -force-d3d11 ``` Require a successful CLI exit and `runtime-keyword-results.json` with @@ -23,7 +23,7 @@ arithmetic or resource bindings equivalent to an original application. To compile every combination of other generated shaders, copy each `.shader` and matching `.variants.json` under a project asset folder, copy -`ShaderVariantChecks.cs` into `Assets/Editor`, and set +`ShaderVariantChecks.cs` and `ShaderVariant*Data.cs` into `Assets/Editor`, and set `SHADER_VARIANT_DIRECTORY` to that folder (for example `Assets/Generated`). Run the same CLI command with `-executeMethod ShaderVariantChecks.Run`. It checks the shader hash, pass count and nonempty compiled bytecode for both diff --git a/tests/UnityRuntime/RuntimeKeywordChecks.cs b/tests/UnityRuntime/RuntimeKeywordChecks.cs index 5117424..395ab94 100644 --- a/tests/UnityRuntime/RuntimeKeywordChecks.cs +++ b/tests/UnityRuntime/RuntimeKeywordChecks.cs @@ -4,63 +4,19 @@ using UnityEngine; using UnityEngine.Rendering; -// Run in a disposable Built-in project after exporting the native xUnit fixture into Assets. +// Checks keyword selection and scope using GPU readbacks in a disposable project. public static class RuntimeKeywordChecks { + // Runs the validation and writes its result report. public static void Run() { - File.WriteAllText("runtime-keyword-results.json", "{\"passed\":false}"); - if (Application.unityVersion != "6000.0.75f1") - throw new InvalidOperationException("Runtime shader checks target Unity 6000.0.75f1."); - if (SystemInfo.graphicsDeviceType != GraphicsDeviceType.Direct3D11) - throw new InvalidOperationException("Runtime shader checks require Direct3D 11."); - AssetDatabase.ImportAsset("Assets/RuntimeKeywords.shader", ImportAssetOptions.ForceSynchronousImport); - Shader shader = AssetDatabase.LoadAssetAtPath("Assets/RuntimeKeywords.shader"); - if (shader == null || ShaderUtil.ShaderHasError(shader)) - throw new InvalidOperationException("Generated shader failed Unity import."); - LocalKeyword mode = new LocalKeyword(shader, "MODE"); - LocalKeyword detail = new LocalKeyword(shader, "DETAIL"); - if (!mode.isValid || !mode.isOverridable || !detail.isValid || detail.isOverridable) - throw new InvalidOperationException("Keyword scopes differ from the serialized metadata."); - Material material = new Material(shader); - RenderTexture target = new RenderTexture(4, 4, 0, RenderTextureFormat.ARGBFloat, RenderTextureReadWrite.Linear); - Texture2D readback = new Texture2D(4, 4, TextureFormat.RGBAFloat, false, true); - Mesh quad = new Mesh - { - vertices = new[] { new Vector3(-1f, -1f, 0f), new Vector3(-1f, 1f, 0f), - new Vector3(1f, 1f, 0f), new Vector3(1f, -1f, 0f) }, - triangles = new[] { 0, 1, 2, 0, 2, 3 }, - }; + (Shader shader, LocalKeyword mode, LocalKeyword detail) = LoadShader(); + (Material material, RenderTexture target, Texture2D readback, Mesh quad) = CreateResources(shader); RenderTexture previous = RenderTexture.active; bool previousSrgb = GL.sRGBWrite; - int cases = 0; try { - if (material.passCount != 2 || material.FindPass("Pass0") != 0 || material.FindPass("Pass1") != 1) - throw new InvalidOperationException("Serialized pass count or order changed."); - Shader.DisableKeyword("MODE"); - Shader.DisableKeyword("DETAIL"); - for (int pass = 0; pass < 2; pass++) - for (int state = 0; state < 4; state++) - { - material.SetKeyword(mode, (state & 1) != 0); - material.SetKeyword(detail, (state & 2) != 0); - Check(material, pass, target, readback, quad, new Color( - (state & 1) != 0 ? 0.75f : 0.25f, - (state & 2) != 0 ? 0.75f : 0.25f, - pass == 0 ? 0.5f : 1f, 1f)); - cases++; - } - material.SetKeyword(mode, false); - material.SetKeyword(detail, false); - Shader.EnableKeyword("MODE"); - Shader.EnableKeyword("DETAIL"); - Check(material, 0, target, readback, quad, new Color(0.75f, 0.25f, 0.5f, 1f)); - cases++; - if (ShaderUtil.ShaderHasError(shader)) - throw new InvalidOperationException("Shader compilation failed while selecting a variant."); - File.WriteAllText("runtime-keyword-results.json", "{\"passed\":true,\"cases\":" + cases + ",\"backend\":\"Direct3D11\"}"); - Debug.Log($"Runtime keyword checks passed: {cases} GPU readbacks."); + CheckCases(shader, material, mode, detail, target, readback, quad); } finally { @@ -75,18 +31,103 @@ public static void Run() } } + // Draws the selected pass and compares its GPU readback with the expected color. private static void Check(Material material, int pass, RenderTexture target, Texture2D readback, Mesh quad, Color expected) { RenderTexture.active = target; GL.sRGBWrite = false; GL.Clear(true, true, Color.magenta); - if (!material.SetPass(pass)) throw new InvalidOperationException("Failed to activate the generated pass."); + if (!material.SetPass(pass)) + { + throw new InvalidOperationException("Failed to activate the generated pass."); + } Graphics.DrawMeshNow(quad, Matrix4x4.identity); readback.ReadPixels(new Rect(0, 0, 4, 4), 0, 0); readback.Apply(); Color actual = readback.GetPixel(2, 2); if (Mathf.Abs(actual.r - expected.r) > 0.001f || Mathf.Abs(actual.g - expected.g) > 0.001f || Mathf.Abs(actual.b - expected.b) > 0.001f || Mathf.Abs(actual.a - expected.a) > 0.001f) + { throw new InvalidOperationException($"Pass {pass}: expected {expected}, read {actual} from the GPU."); + } + } + + // Creates the resources used for GPU readback checks. + private static (Material Material, RenderTexture Target, Texture2D Readback, Mesh Quad) CreateResources(Shader shader) + { + Material material = new Material(shader); + RenderTexture target = new RenderTexture(4, 4, 0, RenderTextureFormat.ARGBFloat, RenderTextureReadWrite.Linear); + Texture2D readback = new Texture2D(4, 4, TextureFormat.RGBAFloat, false, true); + Mesh quad = new Mesh + { + vertices = new[] { new Vector3(-1f, -1f, 0f), new Vector3(-1f, 1f, 0f), + new Vector3(1f, 1f, 0f), new Vector3(1f, -1f, 0f) }, + triangles = new[] { 0, 1, 2, 0, 2, 3 }, + }; + return (material, target, readback, quad); + } + + // Checks local and global keyword selection across all fixture passes. + private static void CheckCases(Shader shader, Material material, LocalKeyword mode, LocalKeyword detail, RenderTexture target, Texture2D readback, Mesh quad) + { + int cases = 0; + if (material.passCount != 2 || material.FindPass("Pass0") != 0 || material.FindPass("Pass1") != 1) + { + throw new InvalidOperationException("Serialized pass count or order changed."); + } + Shader.DisableKeyword("MODE"); + Shader.DisableKeyword("DETAIL"); + for (int pass = 0; pass < 2; pass++) + { + for (int state = 0; state < 4; state++) + { + material.SetKeyword(mode, (state & 1) != 0); + material.SetKeyword(detail, (state & 2) != 0); + Check(material, pass, target, readback, quad, new Color( + (state & 1) != 0 ? 0.75f : 0.25f, + (state & 2) != 0 ? 0.75f : 0.25f, + pass == 0 ? 0.5f : 1f, 1f)); + cases++; + } + } + material.SetKeyword(mode, false); + material.SetKeyword(detail, false); + Shader.EnableKeyword("MODE"); + Shader.EnableKeyword("DETAIL"); + Check(material, 0, target, readback, quad, new Color(0.75f, 0.25f, 0.5f, 1f)); + cases++; + if (ShaderUtil.ShaderHasError(shader)) + { + throw new InvalidOperationException("Shader compilation failed while selecting a variant."); + } + File.WriteAllText("runtime-keyword-results.json", "{\"passed\":true,\"cases\":" + cases + ",\"backend\":\"Direct3D11\"}"); + Debug.Log($"Runtime keyword checks passed: {cases} GPU readbacks."); + } + + // Loads the fixture and verifies the Editor, backend and keyword scopes. + private static (Shader Shader, LocalKeyword Mode, LocalKeyword Detail) LoadShader() + { + File.WriteAllText("runtime-keyword-results.json", "{\"passed\":false}"); + if (Application.unityVersion != "6000.0.75f1") + { + throw new InvalidOperationException("Runtime shader checks target Unity 6000.0.75f1."); + } + if (SystemInfo.graphicsDeviceType != GraphicsDeviceType.Direct3D11) + { + throw new InvalidOperationException("Runtime shader checks require Direct3D 11."); + } + AssetDatabase.ImportAsset("Assets/RuntimeKeywords.shader", ImportAssetOptions.ForceSynchronousImport); + Shader shader = AssetDatabase.LoadAssetAtPath("Assets/RuntimeKeywords.shader"); + if (shader == null || ShaderUtil.ShaderHasError(shader)) + { + throw new InvalidOperationException("Generated shader failed Unity import."); + } + LocalKeyword mode = new LocalKeyword(shader, "MODE"); + LocalKeyword detail = new LocalKeyword(shader, "DETAIL"); + if (!mode.isValid || !mode.isOverridable || !detail.isValid || detail.isOverridable) + { + throw new InvalidOperationException("Keyword scopes differ from the serialized metadata."); + } + return (shader, mode, detail); } } diff --git a/tests/UnityRuntime/ShaderVariantChecks.cs b/tests/UnityRuntime/ShaderVariantChecks.cs index 3c3812c..d34435e 100644 --- a/tests/UnityRuntime/ShaderVariantChecks.cs +++ b/tests/UnityRuntime/ShaderVariantChecks.cs @@ -7,76 +7,109 @@ using UnityEngine; using UnityEngine.Rendering; +// Compiles every variant described by a generated shader manifest. public static class ShaderVariantChecks { - [Serializable] private sealed class Manifest - { - public int schemaVersion; - public string shaderSha256; - public Pass[] passes; - } - [Serializable] private sealed class Pass - { - public int passIndex; - public Variant[] variants; - } - [Serializable] private sealed class Variant { public string[] keywords; } - + // Runs the validation and writes its result report. public static void Run() { File.WriteAllText("shader-variant-results.json", "{\"passed\":false}"); if (Application.unityVersion != "6000.0.75f1" || SystemInfo.graphicsDeviceType != GraphicsDeviceType.Direct3D11) + { throw new InvalidOperationException("Variant checks require Unity 6000.0.75f1 and Direct3D 11."); + } string folder = Environment.GetEnvironmentVariable("SHADER_VARIANT_DIRECTORY"); if (string.IsNullOrEmpty(folder) || !folder.Replace('\\', '/').StartsWith("Assets/", StringComparison.Ordinal)) + { throw new InvalidOperationException("SHADER_VARIANT_DIRECTORY must name a folder under Assets/."); + } string[] paths = Directory.GetFiles(folder, "*.shader", SearchOption.AllDirectories); - if (paths.Length == 0) throw new InvalidOperationException("No generated shaders found."); + if (paths.Length == 0) + { + throw new InvalidOperationException("No generated shaders found."); + } int compiled = 0; foreach (string path in paths.OrderBy(value => value, StringComparer.Ordinal)) { - Manifest manifest = JsonUtility.FromJson(File.ReadAllText(Path.ChangeExtension(path, ".variants.json"))); - using (SHA256 hasher = SHA256.Create()) + compiled += CompileShader(path); + } + File.WriteAllText("shader-variant-results.json", "{\"passed\":true,\"shaders\":" + paths.Length + + ",\"compiledStages\":" + compiled + ",\"editor\":\"" + Application.unityVersion + "\"}"); + } + + // Imports one shader and compiles every variant recorded in its manifest. + private static int CompileShader(string path) + { + int compiled = 0; + ShaderVariantManifestData manifest = ReadManifest(path); + string assetPath = path.Replace('\\', '/'); + AssetDatabase.ImportAsset(assetPath, ImportAssetOptions.ForceSynchronousImport); + Shader shader = AssetDatabase.LoadAssetAtPath(assetPath); + if (shader == null || ShaderUtil.ShaderHasError(shader)) + { + throw new InvalidOperationException("Shader import failed: " + path); + } + ShaderData data = ShaderUtil.GetShaderData(shader); + int flatPass = 0; + for (int sub = 0; sub < data.SubshaderCount; sub++) + { + ShaderData.Subshader subshader = data.GetSubshader(sub); + for (int pass = 0; pass < subshader.PassCount; pass++, flatPass++) { - string hash = BitConverter.ToString(hasher.ComputeHash(File.ReadAllBytes(path))).Replace("-", ""); - if (manifest.schemaVersion != 1 || !string.Equals(hash, manifest.shaderSha256, StringComparison.OrdinalIgnoreCase)) - throw new InvalidOperationException("Manifest does not match shader: " + path); + compiled += CompilePass(path, manifest, flatPass, subshader, pass); } - string assetPath = path.Replace('\\', '/'); - AssetDatabase.ImportAsset(assetPath, ImportAssetOptions.ForceSynchronousImport); - Shader shader = AssetDatabase.LoadAssetAtPath(assetPath); - if (shader == null || ShaderUtil.ShaderHasError(shader)) - throw new InvalidOperationException("Shader import failed: " + path); - ShaderData data = ShaderUtil.GetShaderData(shader); - int flatPass = 0; - for (int sub = 0; sub < data.SubshaderCount; sub++) + } + if (flatPass != manifest.passes.Length || ShaderUtil.ShaderHasError(shader)) + { + throw new InvalidOperationException("Imported pass count or compilation differs from manifest."); + } + Debug.Log($"Validated {path}: {flatPass} passes."); + return compiled; + } + + // Checks that the manifest hash matches the shader source. + private static ShaderVariantManifestData ReadManifest(string path) + { + ShaderVariantManifestData manifest = JsonUtility.FromJson(File.ReadAllText(Path.ChangeExtension(path, ".variants.json"))); + using (SHA256 hasher = SHA256.Create()) + { + string hash = BitConverter.ToString(hasher.ComputeHash(File.ReadAllBytes(path))).Replace("-", ""); + if (manifest.schemaVersion != 1 || !string.Equals(hash, manifest.shaderSha256, StringComparison.OrdinalIgnoreCase)) { - ShaderData.Subshader subshader = data.GetSubshader(sub); - for (int pass = 0; pass < subshader.PassCount; pass++, flatPass++) + throw new InvalidOperationException("Manifest does not match shader: " + path); + } + } + return manifest; + } + + // Compiles both stages of every variant in one imported pass. + private static int CompilePass(string path, ShaderVariantManifestData manifest, int flatPass, ShaderData.Subshader subshader, int pass) + { + int compiled = 0; + if (flatPass >= manifest.passes.Length || manifest.passes[flatPass].passIndex != flatPass) + { + throw new InvalidOperationException("Imported pass order differs from manifest."); + } + ShaderVariantPassData entry = manifest.passes[flatPass]; + if (entry.variants == null || entry.variants.Length == 0) + { + throw new InvalidOperationException("Pass has no variants."); + } + foreach (ShaderVariantData variant in entry.variants) + { + foreach (ShaderType stage in new[] { ShaderType.Vertex, ShaderType.Fragment }) + { + ShaderData.VariantCompileInfo result = subshader.GetPass(pass).CompileVariant(stage, + variant.keywords, ShaderCompilerPlatform.D3D, BuildTarget.StandaloneWindows64); + if (!result.Success || result.ShaderData == null || result.ShaderData.Length == 0) { - if (flatPass >= manifest.passes.Length || manifest.passes[flatPass].passIndex != flatPass) - throw new InvalidOperationException("Imported pass order differs from manifest."); - Pass entry = manifest.passes[flatPass]; - if (entry.variants == null || entry.variants.Length == 0) - throw new InvalidOperationException("Pass has no variants."); - foreach (Variant variant in entry.variants) - foreach (ShaderType stage in new[] { ShaderType.Vertex, ShaderType.Fragment }) - { - ShaderData.VariantCompileInfo result = subshader.GetPass(pass).CompileVariant(stage, - variant.keywords, ShaderCompilerPlatform.D3D, BuildTarget.StandaloneWindows64); - if (!result.Success || result.ShaderData == null || result.ShaderData.Length == 0) - throw new InvalidOperationException($"{path} pass {flatPass} {stage} [{string.Join(",", variant.keywords)}]: " - + string.Join("; ", result.Messages.Select(message => message.message))); - compiled++; - } - Debug.Log($"Compiled {path} pass {flatPass}: {entry.variants.Length} variants."); + throw new InvalidOperationException($"{path} pass {flatPass} {stage} [{string.Join(",", variant.keywords)}]: " + + string.Join("; ", result.Messages.Select(message => message.message))); } + compiled++; } - if (flatPass != manifest.passes.Length || ShaderUtil.ShaderHasError(shader)) - throw new InvalidOperationException("Imported pass count or compilation differs from manifest."); - Debug.Log($"Validated {path}: {flatPass} passes."); } - File.WriteAllText("shader-variant-results.json", "{\"passed\":true,\"shaders\":" + paths.Length - + ",\"compiledStages\":" + compiled + ",\"editor\":\"" + Application.unityVersion + "\"}"); + Debug.Log($"Compiled {path} pass {flatPass}: {entry.variants.Length} variants."); + return compiled; } } diff --git a/tests/UnityRuntime/ShaderVariantData.cs b/tests/UnityRuntime/ShaderVariantData.cs new file mode 100644 index 0000000..7ca5846 --- /dev/null +++ b/tests/UnityRuntime/ShaderVariantData.cs @@ -0,0 +1,8 @@ +using System; + +// Stores serialized variant validation data. +[Serializable] +internal sealed class ShaderVariantData +{ + public string[] keywords; +} diff --git a/tests/UnityRuntime/ShaderVariantManifestData.cs b/tests/UnityRuntime/ShaderVariantManifestData.cs new file mode 100644 index 0000000..0516b92 --- /dev/null +++ b/tests/UnityRuntime/ShaderVariantManifestData.cs @@ -0,0 +1,10 @@ +using System; + +// Stores serialized variant validation data. +[Serializable] +internal sealed class ShaderVariantManifestData +{ + public int schemaVersion; + public string shaderSha256; + public ShaderVariantPassData[] passes; +} diff --git a/tests/UnityRuntime/ShaderVariantPassData.cs b/tests/UnityRuntime/ShaderVariantPassData.cs new file mode 100644 index 0000000..bbbc534 --- /dev/null +++ b/tests/UnityRuntime/ShaderVariantPassData.cs @@ -0,0 +1,9 @@ +using System; + +// Stores serialized variant validation data. +[Serializable] +internal sealed class ShaderVariantPassData +{ + public int passIndex; + public ShaderVariantData[] variants; +} diff --git a/tests/UnityShaderDecompiler.Tests/RuntimeVariantEmissionTests.cs b/tests/UnityShaderDecompiler.Tests/RuntimeVariantEmissionTests.cs index a9c1113..e85fd6b 100644 --- a/tests/UnityShaderDecompiler.Tests/RuntimeVariantEmissionTests.cs +++ b/tests/UnityShaderDecompiler.Tests/RuntimeVariantEmissionTests.cs @@ -6,23 +6,25 @@ namespace UnityShaderDecompiler.Tests; +// Covers runtime variant emission tests. public sealed class RuntimeVariantEmissionTests { + // Verifies that default mode keeps log and continue for a failed serialized program. [Fact] [Trait("Category", "D3D")] public void DefaultModeKeepsLogAndContinueForAFailedSerializedProgram() { - if (!OperatingSystem.IsWindows()) return; - using TempDirectory folder = new TempDirectory(); - List<(ShaderGpuProgramType Stage, byte[] Code)> programs = new List<(ShaderGpuProgramType, byte[])> + if (!OperatingSystem.IsWindows()) { - (ShaderGpuProgramType.DX11VertexSM40, "DXBC"u8.ToArray()), - (ShaderGpuProgramType.DX11PixelSM40, UnityShaderDecompiler.Core.DXBC.Compilation.DxbcCompiler.Compile("float4 main():SV_Target { return 1; }", "main", "ps_4_0")), - }; + return; + } + using TempDirectory folder = new TempDirectory(); + List<(ShaderGpuProgramType Stage, byte[] Code)> programs = CompilePartialPrograms(); File.WriteAllBytes(Path.Combine(folder.Path, "blob.bin"), BuildBlob(programs)); File.WriteAllText(Path.Combine(folder.Path, "metadata.json"), JsonSerializer.Serialize(new { - name = "Test/PartialInput", subShaders = new[] { new { passes = new[] { new + name = "Test/PartialInput", + subShaders = new[] { new { passes = new[] { new { directXPrograms = new[] { @@ -37,11 +39,15 @@ public void DefaultModeKeepsLogAndContinueForAFailedSerializedProgram() new DecompileOptions { RestoreKeywordVariants = true, Verbose = false })); } + // Verifies that default mode retains geometry programs with serialized references. [Fact] [Trait("Category", "D3D")] public void DefaultModeRetainsGeometryProgramsWithSerializedReferences() { - if (!OperatingSystem.IsWindows()) return; + if (!OperatingSystem.IsWindows()) + { + return; + } using TempDirectory folder = new TempDirectory(); const string varying = "struct V { float4 position:SV_POSITION; float marker:TEXCOORD0; }; "; List<(ShaderGpuProgramType Stage, byte[] Code)> programs = new List<(ShaderGpuProgramType, byte[])> @@ -56,7 +62,8 @@ public void DefaultModeRetainsGeometryProgramsWithSerializedReferences() File.WriteAllBytes(Path.Combine(folder.Path, "blob.bin"), BuildBlob(programs)); File.WriteAllText(Path.Combine(folder.Path, "metadata.json"), JsonSerializer.Serialize(new { - name = "Test/GeometryVariants", subShaders = new[] { new { passes = new[] { new + name = "Test/GeometryVariants", + subShaders = new[] { new { passes = new[] { new { name = "Geometry", directXPrograms = new[] { @@ -74,45 +81,26 @@ public void DefaultModeRetainsGeometryProgramsWithSerializedReferences() Assert.NotNull(result.Passes[0].FragmentFunction); } + // Verifies that native programs keep their passes and select by keywords. [Fact] [Trait("Category", "D3D")] public void NativeProgramsKeepTheirPassesAndSelectByKeywords() { - if (!OperatingSystem.IsWindows()) return; + if (!OperatingSystem.IsWindows()) + { + return; + } using TempDirectory folder = new TempDirectory(); - List<(ShaderGpuProgramType Stage, byte[] Code)> programs = new List<(ShaderGpuProgramType, byte[])>(); - const string varying = "struct Varyings { float4 position:SV_POSITION; float4 marker:TEXCOORD0; }; "; - foreach (string value in new[] { "0.25", "0.75" }) - programs.Add((ShaderGpuProgramType.DX11VertexSM40, UnityShaderDecompiler.Core.DXBC.Compilation.DxbcCompiler.Compile( - varying + $"Varyings main(float4 p:POSITION) {{ Varyings o; o.marker=float4({value},0,0,1); o.position=p; return o; }}", "main", "vs_4_0"))); - foreach (string blue in new[] { "0.5", "1.0" }) - foreach (string green in new[] { "0.25", "0.75" }) - programs.Add((ShaderGpuProgramType.DX11PixelSM40, UnityShaderDecompiler.Core.DXBC.Compilation.DxbcCompiler.Compile( - varying + $"float4 main(Varyings i):SV_Target {{ return float4(i.marker.x,{green},{blue},1); }}", "main", "ps_4_0"))); + List<(ShaderGpuProgramType Stage, byte[] Code)> programs = CompileKeywordPrograms(); File.WriteAllBytes(Path.Combine(folder.Path, "blob.bin"), BuildBlob(programs)); - List passes = new List(); - for (int pass = 0; pass < 2; pass++) - passes.Add(new - { - name = "Pass" + pass, - renderState = new { cull = 0, zTest = 8, zWrite = false }, - directXPrograms = new object[] - { - new { blobIndex = 0, stage = "Vertex", keywords = Array.Empty(), keywordMask = new[] { "MODE" } }, - new { blobIndex = 1, stage = "Vertex", keywords = new[] { "MODE" }, keywordMask = new[] { "MODE" } }, - new { blobIndex = 2 + 2 * pass, stage = "Fragment", keywords = Array.Empty(), keywordMask = new[] { "DETAIL" } }, - new { blobIndex = 3 + 2 * pass, stage = "Fragment", keywords = new[] { "DETAIL" }, keywordMask = new[] { "DETAIL" } }, - }, - }); - string metadata = JsonSerializer.Serialize(new - { - name = "Test/RuntimeKeywords", keywordFlags = new { MODE = 0, DETAIL = 1 }, - subShaders = new[] { new { passes } }, - }); + string metadata = BuildKeywordMetadata(); File.WriteAllText(Path.Combine(folder.Path, "metadata.json"), metadata); DecompilationResult result = Decompiler.DecompileFolder(folder.Path, new DecompileOptions { - RestoreKeywordVariants = true, ReconstructSurfaceShaders = false, FuseTemps = false, Verbose = false, + RestoreKeywordVariants = true, + ReconstructSurfaceShaders = false, + FuseTemps = false, + Verbose = false, OutRoot = Path.Combine(folder.Path, "generated"), }); Assert.Equal(2, result.Passes.Count); @@ -122,22 +110,8 @@ public void NativeProgramsKeepTheirPassesAndSelectByKeywords() Assert.Contains("#pragma multi_compile __ MODE", result.ShaderText); Assert.Contains("#error No serialized program matches", result.ShaderText); string shaderPath = Path.Combine(folder.Path, "generated", "Test_RuntimeKeywords.shader"); - List cases = RuntimeVariantManifest.Read(shaderPath); - Assert.Equal(8, cases.Count); - Assert.All(cases, item => Assert.Equal("vs_4_0", item.VertexProfile)); - RuntimeVariantManifest.VerifyCompilation(shaderPath); - File.AppendAllText(shaderPath, "\n// Changed shader\n"); - Assert.Throws(() => RuntimeVariantManifest.Read(shaderPath)); - - foreach (Match match in Regex.Matches(result.ShaderText, @"HLSLPROGRAM(.*?)ENDHLSL", RegexOptions.Singleline)) - for (int state = 0; state < 4; state++) - { - string definitions = ((state & 1) != 0 ? "#define MODE 1\n" : "") - + ((state & 2) != 0 ? "#define DETAIL 1\n" : ""); - Assert.NotEmpty(UnityShaderDecompiler.Core.DXBC.Compilation.DxbcCompiler.Compile(definitions + match.Groups[1].Value, "vert", "vs_4_0")); - Assert.NotEmpty(UnityShaderDecompiler.Core.DXBC.Compilation.DxbcCompiler.Compile(definitions + match.Groups[1].Value, "frag", "ps_4_0")); - } - + VerifyManifest(shaderPath); + VerifyAllKeywordCompilations(result.ShaderText); // Optional artifact export lets the Unity GPU test consume this exact generated shader. string? output = Environment.GetEnvironmentVariable("SHADER_RUNTIME_TEST_OUTPUT"); if (!string.IsNullOrEmpty(output)) @@ -147,6 +121,7 @@ public void NativeProgramsKeepTheirPassesAndSelectByKeywords() } } + // Serializes compiled fixture programs into a Unity shader blob. private static byte[] BuildBlob(List<(ShaderGpuProgramType Stage, byte[] Code)> programs) { List payloads = new List(); @@ -161,7 +136,10 @@ private static byte[] BuildBlob(List<(ShaderGpuProgramType Stage, byte[] Code)> writer.Write(code.Length + 1); writer.Write((byte)0); // Raw Unity variant wrapper. writer.Write(code); - while (payload.Length % 4 != 0) writer.Write((byte)0); + while (payload.Length % 4 != 0) + { + writer.Write((byte)0); + } payloads.Add(payload.ToArray()); } using MemoryStream blob = new MemoryStream(); @@ -175,7 +153,96 @@ private static byte[] BuildBlob(List<(ShaderGpuProgramType Stage, byte[] Code)> table.Write(0); offset += payload.Length; } - foreach (byte[] payload in payloads) table.Write(payload); + foreach (byte[] payload in payloads) + { + table.Write(payload); + } return blob.ToArray(); } + + // Builds one malformed vertex program and one valid fragment program. + private static List<(ShaderGpuProgramType Stage, byte[] Code)> CompilePartialPrograms() + { + List<(ShaderGpuProgramType Stage, byte[] Code)> programs = new List<(ShaderGpuProgramType, byte[])> + { + (ShaderGpuProgramType.DX11VertexSM40, "DXBC"u8.ToArray()), + (ShaderGpuProgramType.DX11PixelSM40, UnityShaderDecompiler.Core.DXBC.Compilation.DxbcCompiler.Compile("float4 main():SV_Target { return 1; }", "main", "ps_4_0")), + }; + return programs; + } + + // Serializes two passes with independent local and global keyword masks. + private static string BuildKeywordMetadata() + { + List passes = new List(); + for (int pass = 0; pass < 2; pass++) + { + passes.Add(new + { + name = "Pass" + pass, + renderState = new { cull = 0, zTest = 8, zWrite = false }, + directXPrograms = new object[] + { + new { blobIndex = 0, stage = "Vertex", keywords = Array.Empty(), keywordMask = new[] { "MODE" } }, + new { blobIndex = 1, stage = "Vertex", keywords = new[] { "MODE" }, keywordMask = new[] { "MODE" } }, + new { blobIndex = 2 + 2 * pass, stage = "Fragment", keywords = Array.Empty(), keywordMask = new[] { "DETAIL" } }, + new { blobIndex = 3 + 2 * pass, stage = "Fragment", keywords = new[] { "DETAIL" }, keywordMask = new[] { "DETAIL" } }, + }, + }); + } + string metadata = JsonSerializer.Serialize(new + { + name = "Test/RuntimeKeywords", + keywordFlags = new { MODE = 0, DETAIL = 1 }, + subShaders = new[] { new { passes } }, + }); + return metadata; + } + + // Compiles independent vertex and fragment keyword states for the native fixture. + private static List<(ShaderGpuProgramType Stage, byte[] Code)> CompileKeywordPrograms() + { + List<(ShaderGpuProgramType Stage, byte[] Code)> programs = new List<(ShaderGpuProgramType, byte[])>(); + const string varying = "struct Varyings { float4 position:SV_POSITION; float4 marker:TEXCOORD0; }; "; + foreach (string value in new[] { "0.25", "0.75" }) + { + programs.Add((ShaderGpuProgramType.DX11VertexSM40, UnityShaderDecompiler.Core.DXBC.Compilation.DxbcCompiler.Compile( + varying + $"Varyings main(float4 p:POSITION) {{ Varyings o; o.marker=float4({value},0,0,1); o.position=p; return o; }}", "main", "vs_4_0"))); + } + foreach (string blue in new[] { "0.5", "1.0" }) + { + foreach (string green in new[] { "0.25", "0.75" }) + { + programs.Add((ShaderGpuProgramType.DX11PixelSM40, UnityShaderDecompiler.Core.DXBC.Compilation.DxbcCompiler.Compile( + varying + $"float4 main(Varyings i):SV_Target {{ return float4(i.marker.x,{green},{blue},1); }}", "main", "ps_4_0"))); + } + } + return programs; + } + + // Checks both stages under each local/global keyword combination. + private static void VerifyAllKeywordCompilations(string shaderText) + { + foreach (Match match in Regex.Matches(shaderText, @"HLSLPROGRAM(.*?)ENDHLSL", RegexOptions.Singleline)) + { + for (int state = 0; state < 4; state++) + { + string definitions = ((state & 1) != 0 ? "#define MODE 1\n" : "") + + ((state & 2) != 0 ? "#define DETAIL 1\n" : ""); + Assert.NotEmpty(UnityShaderDecompiler.Core.DXBC.Compilation.DxbcCompiler.Compile(definitions + match.Groups[1].Value, "vert", "vs_4_0")); + Assert.NotEmpty(UnityShaderDecompiler.Core.DXBC.Compilation.DxbcCompiler.Compile(definitions + match.Groups[1].Value, "frag", "ps_4_0")); + } + } + } + + // Checks exhaustive compilation cases and rejects a changed shader hash. + private static void VerifyManifest(string shaderPath) + { + List cases = RuntimeVariantManifest.Read(shaderPath); + Assert.Equal(8, cases.Count); + Assert.All(cases, item => Assert.Equal("vs_4_0", item.VertexProfile)); + RuntimeVariantManifest.VerifyCompilation(shaderPath); + File.AppendAllText(shaderPath, "\n// Changed shader\n"); + Assert.Throws(() => RuntimeVariantManifest.Read(shaderPath)); + } } diff --git a/tests/UnityShaderDecompiler.Tests/RuntimeVariantPlanTests.cs b/tests/UnityShaderDecompiler.Tests/RuntimeVariantPlanTests.cs index 8f9c801..a15d739 100644 --- a/tests/UnityShaderDecompiler.Tests/RuntimeVariantPlanTests.cs +++ b/tests/UnityShaderDecompiler.Tests/RuntimeVariantPlanTests.cs @@ -3,12 +3,14 @@ namespace UnityShaderDecompiler.Tests; +// Covers runtime variant plan tests. public sealed class RuntimeVariantPlanTests { + // Verifies that independent and exclusive keywords reconstruct exactly the serialized states. [Fact] public void IndependentAndExclusiveKeywordsReconstructExactlyTheSerializedStates() { - string[][] states = [ ["RED"], ["BLUE"], ["RED", "DETAIL"], ["BLUE", "DETAIL"] ]; + string[][] states = [["RED"], ["BLUE"], ["RED", "DETAIL"], ["BLUE", "DETAIL"]]; RuntimeVariantPlan plan = Plan(states, ["RED", "BLUE", "DETAIL", "STRIPPED"]); Assert.Equal(4, plan.States.Count); Assert.Contains(plan.Groups, group => group.Keywords.SequenceEqual(new[] { "BLUE", "RED" }) && !group.AllowsDisabled); @@ -16,18 +18,21 @@ public void IndependentAndExclusiveKeywordsReconstructExactlyTheSerializedStates Assert.Equal(new[] { "STRIPPED" }, plan.StrippedKeywords); } + // Verifies that correlated keywords are rejected instead of inventing combinations. [Fact] public void CorrelatedKeywordsAreRejectedInsteadOfInventingCombinations() { Assert.Throws(() => Plan([[], ["A", "B"]], ["A", "B"])); } + // Verifies that incomplete exclusive sets are rejected. [Fact] public void IncompleteExclusiveSetsAreRejected() { Assert.Throws(() => Plan([[], ["A"], ["B"], ["A", "C"]], ["A", "B", "C"])); } + // Verifies that duplicate state with different programs is ambiguous. [Fact] public void DuplicateStateWithDifferentProgramsIsAmbiguous() { @@ -35,6 +40,7 @@ public void DuplicateStateWithDifferentProgramsIsAmbiguous() Assert.Throws(() => RuntimeVariantPlan.Build(programs, new Dictionary())); } + // Verifies that one program can serve multiple keyword states. [Fact] public void OneProgramCanServeMultipleKeywordStates() { @@ -44,6 +50,7 @@ public void OneProgramCanServeMultipleKeywordStates() Assert.Equal(2, plan.States.Count); } + // Verifies that reused blob does not hide an unmatched keyword state. [Fact] public void ReusedBlobDoesNotHideAnUnmatchedKeywordState() { @@ -53,6 +60,7 @@ public void ReusedBlobDoesNotHideAnUnmatchedKeywordState() new Dictionary { ["MODE"] = 0 })); } + // Verifies that keywords must be preprocessor identifiers. [Theory] [InlineData("BAD NAME")] [InlineData("NAME\n#error injected")] @@ -62,6 +70,7 @@ public void KeywordsMustBePreprocessorIdentifiers(string keyword) Assert.Throws(() => Plan([[], [keyword]], [keyword])); } + // Verifies that missing scope metadata is not guessed. [Fact] public void MissingScopeMetadataIsNotGuessed() { @@ -69,6 +78,7 @@ public void MissingScopeMetadataIsNotGuessed() Assert.Throws(() => RuntimeVariantPlan.Build(programs, new Dictionary())); } + // Verifies that masks must be consistent within each stage. [Fact] public void MasksMustBeConsistentWithinEachStage() { @@ -76,6 +86,7 @@ public void MasksMustBeConsistentWithinEachStage() Assert.Throws(() => VariantPairing.Pair(programs)); } + // Verifies that unknown scope flags are rejected. [Theory] [InlineData(2)] [InlineData(255)] @@ -86,6 +97,7 @@ public void UnknownScopeFlagsAreRejected(int flags) new Dictionary { ["MODE"] = flags })); } + // Verifies that exclusive groups cannot mix local and global keywords. [Fact] public void ExclusiveGroupsCannotMixLocalAndGlobalKeywords() { @@ -95,6 +107,7 @@ public void ExclusiveGroupsCannotMixLocalAndGlobalKeywords() new Dictionary { ["A"] = 0, ["B"] = 1 })); } + // Verifies that disabled local keywords retain their scope. [Fact] public void DisabledLocalKeywordsRetainTheirScope() { @@ -104,6 +117,7 @@ public void DisabledLocalKeywordsRetainTheirScope() Assert.Contains("MODE", plan.LocalKeywords); } + // Builds a keyword plan from the supplied fixture metadata. private static RuntimeVariantPlan Plan(string[][] states, string[] mask) { List programs = new List(); diff --git a/tests/UnityShaderDecompiler.Tests/VariantInterfaceTests.cs b/tests/UnityShaderDecompiler.Tests/VariantInterfaceTests.cs index adf321f..4908058 100644 --- a/tests/UnityShaderDecompiler.Tests/VariantInterfaceTests.cs +++ b/tests/UnityShaderDecompiler.Tests/VariantInterfaceTests.cs @@ -3,8 +3,10 @@ namespace UnityShaderDecompiler.Tests; +// Covers variant interface tests. public sealed class VariantInterfaceTests { + // Verifies that adjacent stages require matching register type and components. [Theory] [InlineData(1, 3, 3, true)] [InlineData(0, 3, 3, false)] @@ -14,11 +16,17 @@ public void AdjacentStagesRequireMatchingRegisterTypeAndComponents(int register, { SignatureElement output = new SignatureElement { - SemanticName = "TEXCOORD", Register = 1, ComponentType = 3, Mask = 3, + SemanticName = "TEXCOORD", + Register = 1, + ComponentType = 3, + Mask = 3, }; SignatureElement input = new SignatureElement { - SemanticName = "texcoord", Register = (uint)register, ComponentType = (uint)type, Mask = (byte)mask, + SemanticName = "texcoord", + Register = (uint)register, + ComponentType = (uint)type, + Mask = (byte)mask, }; Assert.Equal(expected, VariantPairing.InterfacesMatch([output], [input])); } diff --git a/tests/UnityShaderDecompiler.Tests/VariantPairingTests.cs b/tests/UnityShaderDecompiler.Tests/VariantPairingTests.cs index c8ac108..2dc12c6 100644 --- a/tests/UnityShaderDecompiler.Tests/VariantPairingTests.cs +++ b/tests/UnityShaderDecompiler.Tests/VariantPairingTests.cs @@ -3,8 +3,10 @@ namespace UnityShaderDecompiler.Tests; +// Covers variant pairing tests. public class VariantPairingTests { + // Verifies that expanded variants preserve geometry and tessellation stages. [Fact] public void ExpandedVariantsPreserveGeometryAndTessellationStages() { @@ -24,6 +26,7 @@ public void ExpandedVariantsPreserveGeometryAndTessellationStages() group.Select(program => program.Stage))); } + // Verifies that expanded variants reject unmatched intermediate stages. [Fact] public void ExpandedVariantsRejectUnmatchedIntermediateStages() { @@ -31,6 +34,7 @@ public void ExpandedVariantsRejectUnmatchedIntermediateStages() Assert.Throws(() => VariantPairing.Group(programs)); } + // Verifies that fragment only keyword reuses the vertex program. [Fact] public void FragmentOnlyKeywordReusesTheVertexProgram() { @@ -43,6 +47,7 @@ public void FragmentOnlyKeywordReusesTheVertexProgram() Assert.Equal(new[] { (1, 2), (1, 3) }, VariantPairing.Pair(programs)); } + // Verifies that shared keyword matches by state instead of serialized order. [Fact] public void SharedKeywordMatchesByStateInsteadOfSerializedOrder() { @@ -56,6 +61,7 @@ public void SharedKeywordMatchesByStateInsteadOfSerializedOrder() Assert.Equal(new[] { (1, 4), (2, 3) }, VariantPairing.Pair(programs)); } + // Verifies that missing variant fails even when its keyword is never enabled in the other stage. [Fact] public void MissingVariantFailsEvenWhenItsKeywordIsNeverEnabledInTheOtherStage() { diff --git a/tests/test_keyword_flags.py b/tests/test_keyword_flags.py index 6813d6d..8b4d458 100644 --- a/tests/test_keyword_flags.py +++ b/tests/test_keyword_flags.py @@ -1,9 +1,12 @@ +# Checks shader extraction and decompilation behavior. import unittest from libs.metadata import ShaderMetadata +# Covers shader metadata regression cases. class KeywordFlagsTests(unittest.TestCase): + # Checks array and base64 keep local and global scope. def test_array_and_base64_keep_local_and_global_scope(self): for flags in ([0, 1], "AAE="): with self.subTest(flags=flags): @@ -11,14 +14,17 @@ def test_array_and_base64_keep_local_and_global_scope(self): "m_KeywordNames": ["GLOBAL", "LOCAL"], "m_KeywordFlags": flags, }), {"GLOBAL": 0, "LOCAL": 1}) + # Checks missing flags are not invented. def test_missing_flags_are_not_invented(self): self.assertEqual(ShaderMetadata._keyword_flags({"m_KeywordNames": ["MODE"]}), {}) + # Checks malformed flags fail. def test_malformed_flags_fail(self): for flags in ([], [0, 1], [True], [-1], [256], "!bad!"): with self.subTest(flags=flags), self.assertRaises(ValueError): ShaderMetadata._keyword_flags({"m_KeywordNames": ["MODE"], "m_KeywordFlags": flags}) + # Checks duplicate names fail. def test_duplicate_names_fail(self): with self.assertRaises(ValueError): ShaderMetadata._keyword_flags({"m_KeywordNames": ["MODE", "MODE"], "m_KeywordFlags": [0, 1]}) diff --git a/tests/test_variant_metadata.py b/tests/test_variant_metadata.py index 0d63ccc..3773cad 100644 --- a/tests/test_variant_metadata.py +++ b/tests/test_variant_metadata.py @@ -1,3 +1,4 @@ +# Checks shader extraction and decompilation behavior. import json import tempfile import unittest @@ -6,7 +7,9 @@ from libs.metadata import ShaderMetadata +# Covers shader metadata regression cases. class ShaderMetadataTests(unittest.TestCase): + # Checks constant buffer sizes survive serialization. def test_constant_buffer_sizes_survive_serialization(self): shader_pass = { "m_NameIndices": {"Parameters": 0}, @@ -39,6 +42,7 @@ def test_constant_buffer_sizes_survive_serialization(self): "size": 0, "variables": [], }) + # Checks variant references keep stage keyword masks. def test_variant_references_keep_stage_keyword_masks(self): shader_pass = {"m_ProgFragment": { "m_SerializedKeywordStateMask": [0, 1], From 2b4e6ad8c22fbfcc0ad6b7e0e72bf1896239a75d Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 16:50:13 +0100 Subject: [PATCH 77/93] Refactor new code to follow project conventions --- .../Core/Hlsl/HlslPrettyPrinter.cs | 4 + src/UnityShaderDecompiler.Core/Decompiler.cs | 2 + .../CbufferEmissionTests.cs | 59 ++++++---- .../GoldenNativeCompilationTests.cs | 9 +- .../ResourceBindingLayoutsTests.cs | 101 ++++++++++++------ .../SamplerBindingTests.cs | 2 + 6 files changed, 122 insertions(+), 55 deletions(-) diff --git a/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslPrettyPrinter.cs b/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslPrettyPrinter.cs index a30f7b9..c45102e 100644 --- a/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslPrettyPrinter.cs +++ b/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslPrettyPrinter.cs @@ -672,7 +672,9 @@ private static void PrintFunction( { if (res.Kind == HlslResourceKind.ConstantBuffer && res.Variables.Any(v => v.Name == $"cb{res.Slot}_values" && v.ArraySize is not null)) + { ctx.RawCbufferSlots.Add((int)res.Slot); + } if (res.Kind == HlslResourceKind.Texture) { ctx.TextureTypeBySlot[(int)res.Slot] = res.TypeHint ?? "Texture2D"; @@ -1668,7 +1670,9 @@ private static string CbufferElementName(IRRegister reg, PrintContext ctx) private static string RenderCbufferRead(IRRegister reg, PrintContext ctx) { if (reg.Indices.Count >= 2 && ctx.RawCbufferSlots.Contains((int)reg.Indices[0])) + { return CbufferElementName(reg, ctx) + MaskOrSwizzleSuffix(reg); + } if (reg.SymbolicName is not null) { string name = reg.SymbolicName; diff --git a/src/UnityShaderDecompiler.Core/Decompiler.cs b/src/UnityShaderDecompiler.Core/Decompiler.cs index 32c124b..7c79334 100644 --- a/src/UnityShaderDecompiler.Core/Decompiler.cs +++ b/src/UnityShaderDecompiler.Core/Decompiler.cs @@ -659,7 +659,9 @@ private static void AttachPassProgram( HlslCBufferVariable? raw = buffer.Variables.FirstOrDefault( v => v.Name == $"cb{buffer.Slot}_values" && v.ArraySize is not null); if (raw is null) + { continue; + } int metadataRegisters = pass.Cbuffers .Where(entry => entry.Key.Slot == (int)buffer.Slot) .Select(entry => (entry.Value.Size + 15) / 16) diff --git a/tests/UnityShaderDecompiler.Tests/CbufferEmissionTests.cs b/tests/UnityShaderDecompiler.Tests/CbufferEmissionTests.cs index e838c4c..fde28cd 100644 --- a/tests/UnityShaderDecompiler.Tests/CbufferEmissionTests.cs +++ b/tests/UnityShaderDecompiler.Tests/CbufferEmissionTests.cs @@ -8,16 +8,42 @@ namespace UnityShaderDecompiler.Tests; +// Covers cbuffer emission tests. public class CbufferEmissionTests { + // Verifies that partial layout preserves buffer identity and raw byte offsets. [Theory] [InlineData("MaterialParameters", "MaterialParameters")] [InlineData("_Globals", "_GlobalsCB_b1")] [Trait("Category", "D3D")] public void PartialLayoutPreservesBufferIdentityAndRawByteOffsets(string name, string emittedName) { - if (!OperatingSystem.IsWindows()) return; + if (!OperatingSystem.IsWindows()) + { + return; + } + + HlslShaderNode shader = CreatePartialShader(name); + string text = HlslPrettyPrinter.Print(shader, fuseTemps: false); + string declaration = Regex.Match(text, @"cbuffer\b.*?};", RegexOptions.Singleline).Value; + Assert.NotEmpty(declaration); + string source = declaration + "\nfloat4 main() : SV_Target { return cb1_values[1]; }"; + DxbcFile compiled = new(); + compiled.Load(UnityShaderDecompiler.Core.DXBC.Compilation.DxbcCompiler.Compile(source, "main", "ps_5_0")); + + RdefConstantBuffer reflected = Assert.Single(compiled.ResourceDefinition!.ConstantBuffers); + Assert.Equal(emittedName, reflected.Name); + RdefVariable storage = Assert.Single(reflected.Variables); + Assert.Equal("cb1_values", storage.Name); + Assert.Equal(0u, storage.Offset); + Assert.Equal(32u, storage.Size); + Assert.Equal(32u, reflected.Size); + Assert.Equal(1u, compiled.ResourceDefinition.ResourceBindings.Single(b => b.Name == emittedName).BindPoint); + } + // Builds a shader with overlapping named and raw constant-buffer entries. + private static HlslShaderNode CreatePartialShader(string name) + { HlslShaderNode shader = new() { Name = "Test/PartialLayout" }; HlslSubShaderNode subShader = new(); HlslPassNode pass = new() @@ -26,33 +52,28 @@ public void PartialLayoutPreservesBufferIdentityAndRawByteOffsets(string name, s }; HlslResourceNode buffer = new() { - Name = name, Kind = HlslResourceKind.ConstantBuffer, Slot = 1, + Name = name, + Kind = HlslResourceKind.ConstantBuffer, + Slot = 1, }; buffer.Variables.Add(new HlslCBufferVariable { - Name = "KnownMember", TypeName = "float4", Offset = 16, Size = 16, + Name = "KnownMember", + TypeName = "float4", + Offset = 16, + Size = 16, }); buffer.Variables.Add(new HlslCBufferVariable { - Name = "cb1_values", TypeName = "float4", Offset = 0, Size = 32, ArraySize = 2, + Name = "cb1_values", + TypeName = "float4", + Offset = 0, + Size = 32, + ArraySize = 2, }); pass.Resources.Add(buffer); subShader.Passes.Add(pass); shader.SubShaders.Add(subShader); - string text = HlslPrettyPrinter.Print(shader, fuseTemps: false); - string declaration = Regex.Match(text, @"cbuffer\b.*?};", RegexOptions.Singleline).Value; - Assert.NotEmpty(declaration); - string source = declaration + "\nfloat4 main() : SV_Target { return cb1_values[1]; }"; - DxbcFile compiled = new(); - compiled.Load(UnityShaderDecompiler.Core.DXBC.Compilation.DxbcCompiler.Compile(source, "main", "ps_5_0")); - - RdefConstantBuffer reflected = Assert.Single(compiled.ResourceDefinition!.ConstantBuffers); - Assert.Equal(emittedName, reflected.Name); - RdefVariable storage = Assert.Single(reflected.Variables); - Assert.Equal("cb1_values", storage.Name); - Assert.Equal(0u, storage.Offset); - Assert.Equal(32u, storage.Size); - Assert.Equal(32u, reflected.Size); - Assert.Equal(1u, compiled.ResourceDefinition.ResourceBindings.Single(b => b.Name == emittedName).BindPoint); + return shader; } } diff --git a/tests/UnityShaderDecompiler.Tests/GoldenNativeCompilationTests.cs b/tests/UnityShaderDecompiler.Tests/GoldenNativeCompilationTests.cs index 63c6f0d..c6b4778 100644 --- a/tests/UnityShaderDecompiler.Tests/GoldenNativeCompilationTests.cs +++ b/tests/UnityShaderDecompiler.Tests/GoldenNativeCompilationTests.cs @@ -4,21 +4,28 @@ namespace UnityShaderDecompiler.Tests; +// Covers golden native compilation tests. public class GoldenNativeCompilationTests { + // Verifies that golden fixture produces compilable stages. [Theory] [InlineData("vert", "vs_5_0")] [InlineData("frag", "ps_5_0")] [Trait("Category", "D3D")] public void GoldenFixtureProducesCompilableStages(string entry, string profile) { - if (!OperatingSystem.IsWindows()) return; + if (!OperatingSystem.IsWindows()) + { + return; + } DecompilationResult result = FixtureCache.Decompile("HOLO_Holo"); MatchCollection blocks = Regex.Matches(result.ShaderText, @"HLSLPROGRAM(.*?)ENDHLSL", RegexOptions.Singleline); Assert.NotEmpty(blocks); foreach (Match block in blocks) + { Assert.NotEmpty(UnityShaderDecompiler.Core.DXBC.Compilation.DxbcCompiler.Compile(block.Groups[1].Value, entry, profile)); + } } } diff --git a/tests/UnityShaderDecompiler.Tests/ResourceBindingLayoutsTests.cs b/tests/UnityShaderDecompiler.Tests/ResourceBindingLayoutsTests.cs index 2fbffca..1c1bdfd 100644 --- a/tests/UnityShaderDecompiler.Tests/ResourceBindingLayoutsTests.cs +++ b/tests/UnityShaderDecompiler.Tests/ResourceBindingLayoutsTests.cs @@ -10,52 +10,21 @@ namespace UnityShaderDecompiler.Tests; +// Covers resource binding layouts tests. public sealed class ResourceBindingLayoutsTests { + // Verifies that partial cbuffer metadata keeps raw register offsets. [Fact] public void PartialCbufferMetadataKeepsRawRegisterOffsets() { - HlslShaderNode shader = new HlslShaderNode { Name = "Test/CbufferLayout" }; - HlslSubShaderNode subShader = new HlslSubShaderNode(); - HlslPassNode pass = new HlslPassNode(); - HlslResourceNode resource = new HlslResourceNode - { - Name = "NamedBuffer", Kind = HlslResourceKind.ConstantBuffer, Slot = 1, - }; - resource.Variables.Add(new HlslCBufferVariable - { - Name = "NamedValue", TypeName = "float4", Offset = 0, Size = 16, - }); - resource.Variables.Add(new HlslCBufferVariable - { - Name = "cb1_values", TypeName = "float4", Offset = 0, Size = 32, ArraySize = 2, - }); - pass.Resources.Add(resource); - IRRegister read = new IRRegister - { - RegisterType = RegisterType.ConstantBuffer, - Type = IRValueType.Float, - Mask = 15, - ComponentMode = Operand.OperandComponentMode.Mask, - }; - read.Indices.Add(1); - read.Indices.Add(0); - HlslFunctionNode function = new HlslFunctionNode { Name = "frag", Stage = HlslShaderStage.Fragment }; - function.Statements.Statements.Add(new HlslAssignmentStatement - { - Destination = Temp(0, 15), - Expression = new IRExpression.RegisterExpression { Register = read }, - }); - pass.FragmentFunction = function; - subShader.Passes.Add(pass); - shader.SubShaders.Add(subShader); - + HlslShaderNode shader = CreatePartialShader(); string source = HlslPrettyPrinter.Print(shader, fuseTemps: false); Assert.Contains("cbuffer NamedBuffer : register(b1) { float4 cb1_values[2]; };", source); Assert.Contains("cb1_values[0]", source); Assert.DoesNotContain("NamedValue", source); } + // Wraps the fixture function in a shader and emits its HLSL. private static string Print(HlslFunctionNode function) { HlslShaderNode shader = new HlslShaderNode { Name = "Test/Scope" }; @@ -65,6 +34,7 @@ private static string Print(HlslFunctionNode function) return HlslPrettyPrinter.Print(shader, fuseTemps: false); } + // Creates a temporary register with the requested index and lane mask. private static IRRegister Temp(uint index, byte mask = 1) { IRRegister register = new IRRegister @@ -76,13 +46,74 @@ private static IRRegister Temp(uint index, byte mask = 1) ComponentMode = Operand.OperandComponentMode.Mask, }; for (int component = 0; component < 4; component++) + { if ((mask & (1 << component)) != 0) + { register.SsaVersion[component] = 1; + } + } return register; } + // Creates a scalar floating-point constant for the fixture. private static IRExpression.ConstantExpression Float(float value) => new() { RawValues = new[] { unchecked((uint)BitConverter.SingleToInt32Bits(value)) }, }; + + // Builds a shader that reads raw storage from a partially described buffer. + private static HlslShaderNode CreatePartialShader() + { + HlslShaderNode shader = new HlslShaderNode { Name = "Test/CbufferLayout" }; + HlslSubShaderNode subShader = new HlslSubShaderNode(); + HlslPassNode pass = new HlslPassNode(); + HlslResourceNode resource = CreatePartialBuffer(); + pass.Resources.Add(resource); + IRRegister read = new IRRegister + { + RegisterType = RegisterType.ConstantBuffer, + Type = IRValueType.Float, + Mask = 15, + ComponentMode = Operand.OperandComponentMode.Mask, + }; + read.Indices.Add(1); + read.Indices.Add(0); + HlslFunctionNode function = new HlslFunctionNode { Name = "frag", Stage = HlslShaderStage.Fragment }; + function.Statements.Statements.Add(new HlslAssignmentStatement + { + Destination = Temp(0, 15), + Expression = new IRExpression.RegisterExpression { Register = read }, + }); + pass.FragmentFunction = function; + subShader.Passes.Add(pass); + shader.SubShaders.Add(subShader); + return shader; + } + + // Creates overlapping named and raw entries for a partial buffer. + private static HlslResourceNode CreatePartialBuffer() + { + HlslResourceNode resource = new HlslResourceNode + { + Name = "NamedBuffer", + Kind = HlslResourceKind.ConstantBuffer, + Slot = 1, + }; + resource.Variables.Add(new HlslCBufferVariable + { + Name = "NamedValue", + TypeName = "float4", + Offset = 0, + Size = 16, + }); + resource.Variables.Add(new HlslCBufferVariable + { + Name = "cb1_values", + TypeName = "float4", + Offset = 0, + Size = 32, + ArraySize = 2, + }); + return resource; + } } diff --git a/tests/UnityShaderDecompiler.Tests/SamplerBindingTests.cs b/tests/UnityShaderDecompiler.Tests/SamplerBindingTests.cs index 83d07e8..31391fe 100644 --- a/tests/UnityShaderDecompiler.Tests/SamplerBindingTests.cs +++ b/tests/UnityShaderDecompiler.Tests/SamplerBindingTests.cs @@ -4,8 +4,10 @@ namespace UnityShaderDecompiler.Tests; +// Covers sampler binding tests. public class SamplerBindingTests { + // Verifies that same recovered name does not merge different sampler slots. [Fact] public void SameRecoveredNameDoesNotMergeDifferentSamplerSlots() { From 3e5aa6e8c6f44145c0bc3beee91a0fd39c1a0ccd Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 16:50:13 +0100 Subject: [PATCH 78/93] Test invariant formatting across ShaderLab properties and render states --- .../InvariantNumericOutputTests.cs | 127 +++++++++++++----- 1 file changed, 96 insertions(+), 31 deletions(-) diff --git a/tests/UnityShaderDecompiler.Tests/InvariantNumericOutputTests.cs b/tests/UnityShaderDecompiler.Tests/InvariantNumericOutputTests.cs index afe2781..e6fa4bc 100644 --- a/tests/UnityShaderDecompiler.Tests/InvariantNumericOutputTests.cs +++ b/tests/UnityShaderDecompiler.Tests/InvariantNumericOutputTests.cs @@ -1,64 +1,129 @@ using System; -using System.Linq; -using UnityShaderDecompiler.Core.Hlsl; +using System.Collections.Generic; +using System.Globalization; +using System.Text.Json; using UnityShaderDecompiler.Core.Hlsl.Ast; -using UnityShaderDecompiler.Core.Analysis; -using UnityShaderDecompiler.Core.Optimizations; -using UnityShaderDecompiler.Core.DXBC.Instructions; using UnityShaderDecompiler.Core.DXBC.IR; using Xunit; namespace UnityShaderDecompiler.Tests; +// Covers invariant numeric formatting in IR constants and emitted ShaderLab. public sealed class InvariantNumericOutputTests { + // Checks scalar and vector constants under cultures that use decimal commas. [Theory] [InlineData("fr-FR")] [InlineData("de-DE")] public void NumericConstantsUseInvariantDecimalSeparators(string culture) { - System.Globalization.CultureInfo previous = System.Globalization.CultureInfo.CurrentCulture; - try + WithCulture(culture, () => { - System.Globalization.CultureInfo.CurrentCulture = new(culture); - Assert.Equal("0.125", Float(0.125f).ToString()); + IRExpression.ConstantExpression scalar = new IRExpression.ConstantExpression + { + RawValues = new[] { BitConverter.SingleToUInt32Bits(0.125f) }, + }; + Assert.Equal("0.125", scalar.ToString()); IRExpression.ConstantExpression vector = new IRExpression.ConstantExpression { Kind = IRExpression.ConstantExpression.ConstantKind.Double, DoubleValues = new[] { 0.125, 1.25 }, }; Assert.Equal("double2(0.125, 1.25)", vector.ToString()); + }); + } + + // Checks both range bounds in the public pretty-printer output. + [Theory] + [InlineData("fr-FR")] + [InlineData("de-DE")] + public void RangeBoundsUseInvariantDecimalSeparators(string culture) + { + WithCulture(culture, () => + { + string text = RenderProperty(new HlslPropertyNode + { + Name = "_Amount", Kind = HlslPropertyKind.Range, + Range = (-0.5f, 2.75f), DefaultComponents = new[] { 0.125f, 0f, 0f, 0f }, + }); + Assert.Contains("_Amount (\"_Amount\", Range(-0.5, 2.75)) = 0.125\n", text); + }); + } + + // Exercises typed defaults and the legacy JSON fallback for every numeric property kind. + public static IEnumerable PropertyCases() + { + foreach (string culture in new[] { "fr-FR", "de-DE" }) + { + foreach (HlslPropertyKind kind in new[] { HlslPropertyKind.Float, HlslPropertyKind.Range, + HlslPropertyKind.Color, HlslPropertyKind.Vector }) + { + yield return new object[] { culture, kind, true }; + yield return new object[] { culture, kind, false }; + } } - finally { System.Globalization.CultureInfo.CurrentCulture = previous; } } - private static string Print(HlslFunctionNode function) + // Checks exact scalar and four-component defaults through the public printer. + [Theory] + [MemberData(nameof(PropertyCases))] + public void PropertyDefaultsUseInvariantDecimalSeparators(string culture, HlslPropertyKind kind, bool typed) { - HlslShaderNode shader = new HlslShaderNode { Name = "Test/Scope" }; - HlslSubShaderNode subShader = new HlslSubShaderNode(); - subShader.Passes.Add(new HlslPassNode { FragmentFunction = function }); - shader.SubShaders.Add(subShader); - return HlslPrettyPrinter.Print(shader, fuseTemps: false); + WithCulture(culture, () => + { + using JsonDocument json = JsonDocument.Parse("{\"x\":0.125,\"y\":-0.5,\"z\":2.75,\"w\":1.25}"); + string text = RenderProperty(new HlslPropertyNode + { + Name = "_Value", Kind = kind, Range = (0f, 3f), + DefaultComponents = typed ? new[] { 0.125f, -0.5f, 2.75f, 1.25f } : Array.Empty(), + DefaultValue = typed ? null : json.RootElement, + }); + string type = kind == HlslPropertyKind.Range ? "Range(0, 3)" : kind.ToString(); + string value = kind is HlslPropertyKind.Color or HlslPropertyKind.Vector + ? "(0.125,-0.5,2.75,1.25)" : "0.125"; + Assert.Contains($"_Value (\"_Value\", {type}) = {value}\n", text); + }); } - private static IRRegister Temp(uint index, byte mask = 1) + // Checks both signed polygon-offset values in an emitted shader pass. + [Theory] + [InlineData("fr-FR")] + [InlineData("de-DE")] + public void RenderStateOffsetUsesInvariantDecimalSeparators(string culture) { - IRRegister register = new IRRegister + WithCulture(culture, () => { - RegisterType = RegisterType.Temp, - Index = index, - Type = IRValueType.Float, - Mask = mask, - ComponentMode = Operand.OperandComponentMode.Mask, - }; - for (int component = 0; component < 4; component++) - if ((mask & (1 << component)) != 0) - register.SsaVersion[component] = 1; - return register; + HlslShaderNode shader = new HlslShaderNode { Name = "Test/NumericOffset" }; + HlslSubShaderNode subshader = new HlslSubShaderNode(); + subshader.Passes.Add(new HlslPassNode + { + State = new HlslRenderState { OffsetFactor = -0.125f, OffsetUnits = 1.25f }, + }); + shader.SubShaders.Add(subshader); + Assert.Contains(" Offset -0.125, 1.25\n", HlslPrettyPrinter.Print(shader)); + }); + } + + // Emits one property without involving unrelated shader-program generation. + private static string RenderProperty(HlslPropertyNode property) + { + HlslShaderNode shader = new HlslShaderNode { Name = "Test/NumericProperty" }; + shader.Properties.Add(property); + return HlslPrettyPrinter.Print(shader); } - private static IRExpression.ConstantExpression Float(float value) => new() + // Restores the caller's culture even when an assertion fails. + private static void WithCulture(string culture, Action assertion) { - RawValues = new[] { unchecked((uint)BitConverter.SingleToInt32Bits(value)) }, - }; + CultureInfo previous = CultureInfo.CurrentCulture; + try + { + CultureInfo.CurrentCulture = new CultureInfo(culture); + assertion(); + } + finally + { + CultureInfo.CurrentCulture = previous; + } + } } From da3b5527fb58cac773414a4bea5b97c3fac2f141 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 16:50:13 +0100 Subject: [PATCH 79/93] Refactor new code to follow project conventions --- .../ExplicitTextureLodTests.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/UnityShaderDecompiler.Tests/ExplicitTextureLodTests.cs b/tests/UnityShaderDecompiler.Tests/ExplicitTextureLodTests.cs index 7871618..591eb0b 100644 --- a/tests/UnityShaderDecompiler.Tests/ExplicitTextureLodTests.cs +++ b/tests/UnityShaderDecompiler.Tests/ExplicitTextureLodTests.cs @@ -10,15 +10,18 @@ namespace UnityShaderDecompiler.Tests; +// Covers explicit texture lod tests. public sealed class ExplicitTextureLodTests { + // Verifies that explicit zero LOD is not replaced by implicit derivatives. [Fact] public void ExplicitZeroLodIsNotReplacedByImplicitDerivatives() { IRExpression.TextureOperationExpression sample = new IRExpression.TextureOperationExpression { Operation = IRExpression.TextureOperation.SampleLevel, - LOD = Float(0), Coordinates = Float(1), + LOD = Float(0), + Coordinates = Float(1), Resource = new IRRegister { RegisterType = RegisterType.Resource, Index = 0 }, Sampler = new IRRegister { RegisterType = RegisterType.Sampler, Index = 0 }, }; @@ -36,6 +39,7 @@ public void ExplicitZeroLodIsNotReplacedByImplicitDerivatives() Assert.Same(sample.LOD, actual.LOD); } + // Wraps the fixture function in a shader and emits its HLSL. private static string Print(HlslFunctionNode function) { HlslShaderNode shader = new HlslShaderNode { Name = "Test/Scope" }; @@ -45,6 +49,7 @@ private static string Print(HlslFunctionNode function) return HlslPrettyPrinter.Print(shader, fuseTemps: false); } + // Creates a temporary register with the requested index and lane mask. private static IRRegister Temp(uint index, byte mask = 1) { IRRegister register = new IRRegister @@ -56,11 +61,16 @@ private static IRRegister Temp(uint index, byte mask = 1) ComponentMode = Operand.OperandComponentMode.Mask, }; for (int component = 0; component < 4; component++) + { if ((mask & (1 << component)) != 0) + { register.SsaVersion[component] = 1; + } + } return register; } + // Creates a scalar floating-point constant for the fixture. private static IRExpression.ConstantExpression Float(float value) => new() { RawValues = new[] { unchecked((uint)BitConverter.SingleToInt32Bits(value)) }, From 6fd9c64bbb58da0fd60a5c6e4662c48ad34757bc Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 16:50:13 +0100 Subject: [PATCH 80/93] Refactor new code to follow project conventions --- Extract.py | 2 +- libs/shader_json.py | 8 ++++++++ tests/test_exporter_schemas.py | 33 ++++++++++++++++++++++++--------- tests/test_metadata_offsets.py | 8 ++++++++ 4 files changed, 41 insertions(+), 10 deletions(-) diff --git a/Extract.py b/Extract.py index a9ac935..8be10b6 100644 --- a/Extract.py +++ b/Extract.py @@ -1,4 +1,4 @@ -""" +""" Stage 0 — Frontend / Preprocessing orchestrator. For a given shader JSON export, produces the three inputs Program.cs (C#) diff --git a/libs/shader_json.py b/libs/shader_json.py index 5827511..ef56e09 100644 --- a/libs/shader_json.py +++ b/libs/shader_json.py @@ -1,8 +1,10 @@ +# Normalizes supported shader JSON export schemas. """Normalize the serialized field names used by AssetStudio and AssetRipper.""" import json +# Reads a byte offset from either supported parameter schema. def parameter_offset(parameter): # Unity renamed this field; AssetRipper also normalizes older exports. field = "m_OffsetInConstantBuffer" if "m_OffsetInConstantBuffer" in parameter else "m_Index" @@ -13,6 +15,7 @@ def parameter_offset(parameter): return offset +# Resolves equivalent field names and rejects conflicting values. def _alias(data, canonical, legacy): if legacy not in data: return @@ -22,6 +25,7 @@ def _alias(data, canonical, legacy): data[canonical] = value +# Normalizes dictionary and serialized key/value map encodings. def _mapping(value, label): if isinstance(value, dict): return value @@ -38,6 +42,7 @@ def _mapping(value, label): return result +# Normalizes serialized shader tags. def _tags(owner): tags = owner.get("m_Tags") if isinstance(tags, dict): @@ -46,6 +51,7 @@ def _tags(owner): tags["m_Tags"] = _mapping(tags["m_Tags"], "tag") +# Unwraps serialized state values without inventing defaults. def _wrapped(value, integer=False): if not isinstance(value, dict): return value @@ -60,6 +66,7 @@ def _wrapped(value, integer=False): return value +# Normalizes render-state fields from both exporter schemas. def _render_state(state): for name in ("zClip", "zTest", "zWrite", "culling", "conservative", "alphaToMask", "stencilReadMask", "stencilWriteMask", "stencilRef", "offsetFactor", @@ -92,6 +99,7 @@ def _render_state(state): _tags(state) +# Loads and normalizes an AssetStudio or AssetRipper shader export. def load_shader_json(path): with open(path, encoding="utf-8-sig") as stream: data = json.load(stream) diff --git a/tests/test_exporter_schemas.py b/tests/test_exporter_schemas.py index 092d86e..489b819 100644 --- a/tests/test_exporter_schemas.py +++ b/tests/test_exporter_schemas.py @@ -1,3 +1,4 @@ +# Checks shader extraction and decompilation behavior. import base64 import copy import json @@ -12,7 +13,9 @@ from libs.shader_json import load_shader_json +# Covers shader metadata regression cases. class ExporterSchemaTests(unittest.TestCase): + # Builds an AssetStudio fixture with compressed bytecode and metadata. def studio(self): payload = b"generic serialized shader data" compressed = lz4.block.compress(payload, store_size=False) @@ -42,27 +45,35 @@ def studio(self): "m_VectorParams": [{"m_NameIndex": 2, "m_Index": 16, "m_Dim": 4}]}], }, }, - "m_State": { - "m_Tags": {"tags": [{"Key": "LIGHTMODE", "Value": "FORWARDBASE"}]}, - "culling": {"name": "", "val": 2.0}, - "zTest": {"val": 4.0}, "zWrite": {"val": 1.0}, - "offsetFactor": {"val": 0.125}, - "rtBlend0": {"srcBlend": {"val": 5.0}, "destBlend": {"val": 10.0}, - "colMask": {"val": 14.0}}, - "stencilOpFront": {"comp": {"val": 8.0}, "pass": {"val": 2.0}}, - }, + "m_State": self.render_state(), }], }], }, } return shader, payload + + # Builds serialized render-state values for the exporter fixture. + def render_state(self): + return { + "m_Tags": {"tags": [{"Key": "LIGHTMODE", "Value": "FORWARDBASE"}]}, + "culling": {"name": "", "val": 2.0}, + "zTest": {"val": 4.0}, "zWrite": {"val": 1.0}, + "offsetFactor": {"val": 0.125}, + "rtBlend0": {"srcBlend": {"val": 5.0}, "destBlend": {"val": 10.0}, + "colMask": {"val": 14.0}}, + "stencilOpFront": {"comp": {"val": 8.0}, "pass": {"val": 2.0}}, + } + + + # Loads one fixture through the JSON, metadata and blob readers. def load(self, shader): with tempfile.TemporaryDirectory() as folder: path = Path(folder) / "shader.json" path.write_text(json.dumps(shader), encoding="utf-8") return load_shader_json(path), ShaderMetadata(path).build(), ShaderBlob(path).get_blob() + # Checks assetstudio fields keep values and bindings. def test_assetstudio_fields_keep_values_and_bindings(self): shader, payload = self.studio() normalized, metadata, blob = self.load(shader) @@ -80,6 +91,7 @@ def test_assetstudio_fields_keep_values_and_bindings(self): self.assertEqual((state["blend"]["srcBlend"], state["blend"]["dstBlend"], state["blend"]["colorMask"]), (5, 10, 14)) self.assertIs(type(state["stencilOpFront"]["m_Comp"]["m_Value"]), int) + # Checks assetripper fields produce identical inputs. def test_assetripper_fields_produce_identical_inputs(self): studio, payload = self.studio() # Construct the alternative schema explicitly, independently of the loader. @@ -112,12 +124,14 @@ def test_assetripper_fields_produce_identical_inputs(self): self.assertEqual(actual, expected) self.assertEqual(blob, payload) + # Checks conflicting aliases fail. def test_conflicting_aliases_fail(self): shader, _ = self.studio() shader["m_CompressedLengths"] = [[1]] with self.assertRaisesRegex(ValueError, "Conflicting shader fields"): self.load(shader) + # Checks duplicate serialized names fail. def test_duplicate_serialized_names_fail(self): shader, _ = self.studio() names = shader["m_ParsedForm"]["m_SubShaders"][0]["m_Passes"][0]["m_NameIndices"] @@ -125,6 +139,7 @@ def test_duplicate_serialized_names_fail(self): with self.assertRaisesRegex(ValueError, "duplicate"): self.load(shader) + # Checks fractional render state enum fails. def test_fractional_render_state_enum_fails(self): shader, _ = self.studio() shader["m_ParsedForm"]["m_SubShaders"][0]["m_Passes"][0]["m_State"]["culling"]["val"] = 1.5 diff --git a/tests/test_metadata_offsets.py b/tests/test_metadata_offsets.py index 1ec26ed..556b367 100644 --- a/tests/test_metadata_offsets.py +++ b/tests/test_metadata_offsets.py @@ -1,3 +1,4 @@ +# Checks shader extraction and decompilation behavior. import json import tempfile import unittest @@ -6,7 +7,9 @@ from libs.metadata import ShaderMetadata +# Covers shader metadata regression cases. class MetadataOffsetTests(unittest.TestCase): + # Builds normalized metadata from a temporary JSON fixture. def extract(self, vector, matrix): shader_pass = { "m_NameIndices": {"Globals": 1, "Tint": 2, "Transform": 3}, @@ -36,28 +39,33 @@ def extract(self, vector, matrix): self.assertEqual(shared, local[0]["variables"]) return [(entry["name"], entry["offset"]) for entry in shared] + # Checks both serialized names preserve byte offsets. def test_both_serialized_names_preserve_byte_offsets(self): for field in ("m_Index", "m_OffsetInConstantBuffer"): with self.subTest(field=field): self.assertEqual(self.extract({field: 20}, {field: 48}), [("Tint", 20), ("Transform", 48)]) + # Checks explicit offset takes precedence. def test_explicit_offset_takes_precedence(self): self.assertEqual(self.extract( {"m_Index": 0, "m_OffsetInConstantBuffer": 20}, {"m_Index": 0, "m_OffsetInConstantBuffer": 48}), [("Tint", 20), ("Transform", 48)]) + # Checks zero is a valid offset. def test_zero_is_a_valid_offset(self): self.assertEqual(self.extract({"m_Index": 0}, {"m_OffsetInConstantBuffer": 0}), [("Tint", 0), ("Transform", 0)]) + # Checks missing offsets fail in both readers. def test_missing_offsets_fail_in_both_readers(self): for vector, matrix in (({}, {"m_Index": 48}), ({"m_Index": 20}, {})): with self.subTest(vector=vector, matrix=matrix): with self.assertRaisesRegex(ValueError, "byte offset"): self.extract(vector, matrix) + # Checks invalid offsets are not replaced by legacy values. def test_invalid_offsets_are_not_replaced_by_legacy_values(self): for value in (None, True, -4, 2, 1.5, "16"): for field in ("m_Index", "m_OffsetInConstantBuffer"): From 2e53cb43da73386599678b7d5ffec160eeb54f8b Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 16:50:14 +0100 Subject: [PATCH 81/93] Clean up failure-policy documentation and tests --- README.md | 3 -- .../DirectXFailureTests.cs | 42 +++++++++++-------- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index d35cf07..0c7526f 100644 --- a/README.md +++ b/README.md @@ -121,9 +121,6 @@ dotnet run --project "src/UnityShaderDecompiler.Cli/UnityShaderDecompiler.Cli.cs The result is written to `src/UnityShaderDecompiler.Cli/Output/HOLO_Holo.shader`. -`--fail-on-error` aborts on a DirectX subprogram failure with its index and cause. -It is off by default, preserving the existing log-and-continue behavior. - ## Usage `Shader Decompiler` accepts a Stage 0 output folder or a `blob.bin` with a diff --git a/tests/UnityShaderDecompiler.Tests/DirectXFailureTests.cs b/tests/UnityShaderDecompiler.Tests/DirectXFailureTests.cs index 0537e78..93cb691 100644 --- a/tests/UnityShaderDecompiler.Tests/DirectXFailureTests.cs +++ b/tests/UnityShaderDecompiler.Tests/DirectXFailureTests.cs @@ -4,8 +4,10 @@ namespace UnityShaderDecompiler.Tests; +// Covers opt-in DirectX failure handling. public class DirectXFailureTests { + // Verifies that malformed DirectX program honors explicit failure policy. [Theory] [InlineData(ShaderGpuProgramType.DX11VertexSM40, true)] [InlineData(ShaderGpuProgramType.DX11PixelSM50, true)] @@ -14,6 +16,29 @@ public class DirectXFailureTests public void MalformedDirectXProgramHonorsExplicitFailurePolicy(ShaderGpuProgramType stage, bool failOnError) { using TempDirectory folder = new(); + WriteInvalidProgram(folder, stage); + string output = Path.Combine(folder.Path, "generated"); + + if (!failOnError) + { + Assert.False(new DecompileOptions().FailOnError); + Decompiler.DecompileFolder(folder.Path, new DecompileOptions { Verbose = false, OutRoot = output }); + Assert.Single(Directory.GetFiles(output, "*.shader")); + return; + } + + InvalidDataException error = Assert.Throws(() => + Decompiler.DecompileFolder(folder.Path, new DecompileOptions { FailOnError = true, Verbose = false, OutRoot = output })); + + Assert.Contains("DirectX subprogram 0", error.Message); + Assert.Contains(stage.ToString(), error.Message); + Assert.NotNull(error.InnerException); + Assert.Empty(Directory.GetFiles(output, "*.shader")); + } + + // Writes a deliberately truncated DirectX program for failure-policy checks. + private static void WriteInvalidProgram(TempDirectory folder, ShaderGpuProgramType stage) + { using MemoryStream payload = new(); using (BinaryWriter writer = new(payload, System.Text.Encoding.UTF8, leaveOpen: true)) { @@ -36,22 +61,5 @@ public void MalformedDirectXProgramHonorsExplicitFailurePolicy(ShaderGpuProgramT File.WriteAllBytes(Path.Combine(folder.Path, "blob.bin"), blob.ToArray()); File.WriteAllText(Path.Combine(folder.Path, "metadata.json"), "{\"name\":\"Test/InvalidDxbc\",\"subShaders\":[]}"); - string output = Path.Combine(folder.Path, "generated"); - - if (!failOnError) - { - Assert.False(new DecompileOptions().FailOnError); - Decompiler.DecompileFolder(folder.Path, new DecompileOptions { Verbose = false, OutRoot = output }); - Assert.Single(Directory.GetFiles(output, "*.shader")); - return; - } - - InvalidDataException error = Assert.Throws(() => - Decompiler.DecompileFolder(folder.Path, new DecompileOptions { FailOnError = true, Verbose = false, OutRoot = output })); - - Assert.Contains("DirectX subprogram 0", error.Message); - Assert.Contains(stage.ToString(), error.Message); - Assert.NotNull(error.InnerException); - Assert.Empty(Directory.GetFiles(output, "*.shader")); } } From e3d55144647d7061004f7ef5857933999b61c185 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 16:50:14 +0100 Subject: [PATCH 82/93] Refactor new code to follow project conventions --- .../Core/Analysis/IRLeaveSsa.cs | 2 + .../Core/Hlsl/HlslPrettyPrinter.cs | 15 +++- .../ConditionalValueScopeTests.cs | 84 +++++++++++-------- 3 files changed, 65 insertions(+), 36 deletions(-) diff --git a/src/UnityShaderDecompiler.Core/Core/Analysis/IRLeaveSsa.cs b/src/UnityShaderDecompiler.Core/Core/Analysis/IRLeaveSsa.cs index ceb5cb2..96e6a6c 100644 --- a/src/UnityShaderDecompiler.Core/Core/Analysis/IRLeaveSsa.cs +++ b/src/UnityShaderDecompiler.Core/Core/Analysis/IRLeaveSsa.cs @@ -74,7 +74,9 @@ private static IRBlock ResolveInsertionTarget(IRBlock pred) // Closing a conditional joins both paths. Copies appended here // must stay after the marker, rather than move into one branch. if (pred.Statements[0] is IRStatement.IREndIf or IRStatement.IREndSwitch) + { break; + } if (pred.Predecessors.Count == 0) break; // nothing real to redirect to (entry block) — leave as-is diff --git a/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslPrettyPrinter.cs b/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslPrettyPrinter.cs index c97289f..8ea8b37 100644 --- a/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslPrettyPrinter.cs +++ b/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslPrettyPrinter.cs @@ -2366,26 +2366,37 @@ private static Dictionary CrossBranchMergeNames(HlslIfStatement iff return result; } + // Collects temporary register names used by an expression. private static void AddNames(HlslBlockStatement block, Dictionary names, PrintContext ctx) { foreach ((string name, int comps) in WrittenDeclNames(block.Statements, ctx)) if (!names.TryGetValue(name, out int prev) || comps > prev) + { names[name] = comps; + } foreach (HlslStatementNode stmt in block.Statements) + { switch (stmt) { case HlslIfStatement nested: AddNames(nested.Then, names, ctx); - if (nested.Else is not null) AddNames(nested.Else, names, ctx); + if (nested.Else is not null) + { + AddNames(nested.Else, names, ctx); + } break; case HlslLoopStatement loop: AddNames(loop.Body, names, ctx); break; case HlslSwitchStatement sw: - foreach (HlslSwitchCase c in sw.Cases) AddNames(c.Body, names, ctx); + foreach (HlslSwitchCase c in sw.Cases) + { + AddNames(c.Body, names, ctx); + } break; } + } } // Names first written at the top level of a loop body that are not yet diff --git a/tests/UnityShaderDecompiler.Tests/ConditionalValueScopeTests.cs b/tests/UnityShaderDecompiler.Tests/ConditionalValueScopeTests.cs index a1a6e02..9c1982c 100644 --- a/tests/UnityShaderDecompiler.Tests/ConditionalValueScopeTests.cs +++ b/tests/UnityShaderDecompiler.Tests/ConditionalValueScopeTests.cs @@ -10,8 +10,10 @@ namespace UnityShaderDecompiler.Tests; +// Covers conditional value scope tests. public sealed class ConditionalValueScopeTests { + // Verifies that phi copy after nested conditional runs after both branches. [Fact] public void PhiCopyAfterNestedConditionalRunsAfterBothBranches() { @@ -34,43 +36,11 @@ public void PhiCopyAfterNestedConditionalRunsAfterBothBranches() Assert.IsType(join.Statements[1]); } + // Verifies that value written in only one branch is declared outside the if. [Fact] public void ValueWrittenInOnlyOneBranchIsDeclaredOutsideTheIf() { - // A value written in one branch and read after the if must be - // declared outside the branch to avoid HLSL X3004. - IRRegister value = Temp(0); - IRRegister merged = Temp(1); - HlslIfStatement branch = new HlslIfStatement - { - Condition = Float(1), - Then = new HlslBlockStatement(), - Else = new HlslBlockStatement(), - }; - branch.Then.Statements.Add(new HlslAssignmentStatement - { - Destination = merged, - Expression = Float(2), - }); - branch.Else.Statements.Add(new HlslAssignmentStatement - { - Destination = value, - Expression = Float(3), - }); - branch.Else.Statements.Add(new HlslAssignmentStatement - { - Destination = merged, - Expression = new IRExpression.RegisterExpression { Register = value }, - }); - - HlslFunctionNode function = new HlslFunctionNode { Name = "frag", Stage = HlslShaderStage.Fragment }; - function.Statements.Statements.Add(branch); - function.Statements.Statements.Add(new HlslAssignmentStatement - { - Destination = Temp(2), - Expression = new IRExpression.RegisterExpression { Register = value }, - }); - + HlslFunctionNode function = BuildConditionalFunction(); string source = Print(function); int declaration = source.IndexOf("float r0_x_1;", StringComparison.Ordinal); int ifStart = source.IndexOf("if (", StringComparison.Ordinal); @@ -79,6 +49,7 @@ public void ValueWrittenInOnlyOneBranchIsDeclaredOutsideTheIf() Assert.DoesNotContain("float r0_x_1 = 3", source); } + // Wraps the fixture function in a shader and emits its HLSL. private static string Print(HlslFunctionNode function) { HlslShaderNode shader = new HlslShaderNode { Name = "Test/Scope" }; @@ -88,6 +59,7 @@ private static string Print(HlslFunctionNode function) return HlslPrettyPrinter.Print(shader, fuseTemps: false); } + // Creates a temporary register with the requested index and lane mask. private static IRRegister Temp(uint index, byte mask = 1) { IRRegister register = new IRRegister @@ -99,13 +71,57 @@ private static IRRegister Temp(uint index, byte mask = 1) ComponentMode = Operand.OperandComponentMode.Mask, }; for (int component = 0; component < 4; component++) + { if ((mask & (1 << component)) != 0) + { register.SsaVersion[component] = 1; + } + } return register; } + // Creates a scalar floating-point constant for the fixture. private static IRExpression.ConstantExpression Float(float value) => new() { RawValues = new[] { unchecked((uint)BitConverter.SingleToInt32Bits(value)) }, }; + + // Builds a value that escapes its conditional branch. + private static HlslFunctionNode BuildConditionalFunction() + { + // A value written in one branch and read after the if must be + // declared outside the branch to avoid HLSL X3004. + IRRegister value = Temp(0); + IRRegister merged = Temp(1); + HlslIfStatement branch = new HlslIfStatement + { + Condition = Float(1), + Then = new HlslBlockStatement(), + Else = new HlslBlockStatement(), + }; + branch.Then.Statements.Add(new HlslAssignmentStatement + { + Destination = merged, + Expression = Float(2), + }); + branch.Else.Statements.Add(new HlslAssignmentStatement + { + Destination = value, + Expression = Float(3), + }); + branch.Else.Statements.Add(new HlslAssignmentStatement + { + Destination = merged, + Expression = new IRExpression.RegisterExpression { Register = value }, + }); + + HlslFunctionNode function = new HlslFunctionNode { Name = "frag", Stage = HlslShaderStage.Fragment }; + function.Statements.Statements.Add(branch); + function.Statements.Statements.Add(new HlslAssignmentStatement + { + Destination = Temp(2), + Expression = new IRExpression.RegisterExpression { Register = value }, + }); + return function; + } } From 47eebe251ba4003c61192da0a8162609deca4f14 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 16:50:14 +0100 Subject: [PATCH 83/93] Refactor new code to follow project conventions --- .../DXBC/Chunks/SFI0 Chunk.cs | 2 ++ tests/UnityShaderDecompiler.Tests/BlobAndDxbcTests.cs | 1 + .../NativeFeatureInfoTests.cs | 10 +++++++++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/UnityShaderDecompiler.Core/DXBC/Chunks/SFI0 Chunk.cs b/src/UnityShaderDecompiler.Core/DXBC/Chunks/SFI0 Chunk.cs index aa59c88..c6126d4 100644 --- a/src/UnityShaderDecompiler.Core/DXBC/Chunks/SFI0 Chunk.cs +++ b/src/UnityShaderDecompiler.Core/DXBC/Chunks/SFI0 Chunk.cs @@ -49,7 +49,9 @@ public class Sfi0Chunk public void Read(BinaryReader reader) { if (reader.BaseStream.Length - reader.BaseStream.Position != sizeof(ulong)) + { throw new InvalidDataException("SFI0 chunk must contain exactly 8 bytes of feature flags."); + } FeatureFlags = reader.ReadUInt64(); } diff --git a/tests/UnityShaderDecompiler.Tests/BlobAndDxbcTests.cs b/tests/UnityShaderDecompiler.Tests/BlobAndDxbcTests.cs index 498ac94..5b264a2 100644 --- a/tests/UnityShaderDecompiler.Tests/BlobAndDxbcTests.cs +++ b/tests/UnityShaderDecompiler.Tests/BlobAndDxbcTests.cs @@ -224,6 +224,7 @@ public void Sfi0Chunk_ExposesFeatureFlags() Assert.Empty(file.UnknownChunks); } + // Verifies that SFI0 chunk rejects invalid length. [Fact] public void Sfi0Chunk_RejectsInvalidLength() { diff --git a/tests/UnityShaderDecompiler.Tests/NativeFeatureInfoTests.cs b/tests/UnityShaderDecompiler.Tests/NativeFeatureInfoTests.cs index 67ec2ac..6c64bb4 100644 --- a/tests/UnityShaderDecompiler.Tests/NativeFeatureInfoTests.cs +++ b/tests/UnityShaderDecompiler.Tests/NativeFeatureInfoTests.cs @@ -5,8 +5,10 @@ namespace UnityShaderDecompiler.Tests; +// Covers native feature info tests. public class NativeFeatureInfoTests { + // Verifies that legacy statistics fields remain available with an explicit availability flag. [Fact] public void LegacyStatisticsFieldsRemainAvailableWithAnExplicitAvailabilityFlag() { @@ -27,14 +29,20 @@ public void LegacyStatisticsFieldsRemainAvailableWithAnExplicitAvailabilityFlag( "CMaxNumThreadsX", "CMaxNumThreadsY", "CMaxNumThreadsZ", }; foreach (string field in fields) + { Assert.Equal(typeof(uint), typeof(UnityShaderDecompiler.Core.DXBC.Chunks.Sfi0Chunk).GetField(field)?.FieldType); + } } + // Verifies that feature flags match the native compilers eight byte payload. [Fact] [Trait("Category", "D3D")] public void FeatureFlagsMatchTheNativeCompilersEightBytePayload() { - if (!OperatingSystem.IsWindows()) return; + if (!OperatingSystem.IsWindows()) + { + return; + } const string source = "float main(float x : TEXCOORD) : SV_Target " + "{ double value = (double)x; return (float)(value * value); }"; From e201c910c42d89d21b16e57f4094669572ef73c6 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 17:34:20 +0100 Subject: [PATCH 84/93] Keep duplicate texture bindings distinct in generated HLSL --- .../Core/Hlsl/HlslPrettyPrinter.cs | 27 ++++++++++++ .../ResourceBindingLayoutsTests.cs | 43 ++++++++++++++----- 2 files changed, 59 insertions(+), 11 deletions(-) diff --git a/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslPrettyPrinter.cs b/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslPrettyPrinter.cs index c45102e..714ec83 100644 --- a/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslPrettyPrinter.cs +++ b/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslPrettyPrinter.cs @@ -196,6 +196,7 @@ private static void PrintPass(StringBuilder sb, HlslPassNode pass, bool fuseTemp // in the function body are redirected. var cbufferRenames = BuildCbufferRenameMap(pass); + EnsureUniqueTextureNames(pass.Resources); foreach (HlslResourceNode res in pass.Resources) PrintResource(sb, res, stripRegisterBindings, cbufferRenames); @@ -273,6 +274,27 @@ private static void PrintRenderState(StringBuilder sb, HlslRenderState s) // ---------- Stage 7: Resources ---------- + // Gives textures on different slots distinct HLSL identifiers in one pass. + private static void EnsureUniqueTextureNames(List resources) + { + HashSet used = new HashSet(StringComparer.Ordinal); + foreach (HlslResourceNode resource in resources) + { + if (resource.Kind != HlslResourceKind.Texture || used.Add(resource.Name)) + { + continue; + } + string stem = $"{resource.Name}_t{resource.Slot}"; + string name = stem; + int suffix = 1; + while (!used.Add(name) || resources.Any(other => !ReferenceEquals(other, resource) && other.Name == name)) + { + name = $"{stem}_{suffix++}"; + } + resource.Name = name; + } + } + // When vert and frag have different variables at the same byte offset // within a merged cbuffer, one must be declared and the other's // references redirected. Returns a map from the dropped variable name @@ -1624,6 +1646,11 @@ private static List MaskIndices(byte mask) private static string BaseIdentifier(IRRegister reg, PrintContext ctx) { + if (reg.RegisterType == RegisterType.Resource + && ctx.TextureNameBySlot.TryGetValue((int)reg.Index, out string? textureName)) + { + return textureName; + } if (reg.SymbolicName is not null) return reg.SymbolicName; diff --git a/tests/UnityShaderDecompiler.Tests/ResourceBindingLayoutsTests.cs b/tests/UnityShaderDecompiler.Tests/ResourceBindingLayoutsTests.cs index 1c1bdfd..86d26d8 100644 --- a/tests/UnityShaderDecompiler.Tests/ResourceBindingLayoutsTests.cs +++ b/tests/UnityShaderDecompiler.Tests/ResourceBindingLayoutsTests.cs @@ -24,14 +24,41 @@ public void PartialCbufferMetadataKeepsRawRegisterOffsets() Assert.DoesNotContain("NamedValue", source); } - // Wraps the fixture function in a shader and emits its HLSL. - private static string Print(HlslFunctionNode function) + // Keeps declarations and reads distinct when two texture slots share a name. + [Fact] + public void DuplicateTextureNamesUseDistinctIdentifiers() { - HlslShaderNode shader = new HlslShaderNode { Name = "Test/Scope" }; + HlslShaderNode shader = new HlslShaderNode { Name = "Test/TextureSlots" }; HlslSubShaderNode subShader = new HlslSubShaderNode(); - subShader.Passes.Add(new HlslPassNode { FragmentFunction = function }); + HlslPassNode pass = new HlslPassNode(); + pass.Resources.Add(new HlslResourceNode { Name = "_Tex", Kind = HlslResourceKind.Texture, Slot = 0 }); + pass.Resources.Add(new HlslResourceNode { Name = "_Tex", Kind = HlslResourceKind.Texture, Slot = 1 }); + IRRegister resource = new IRRegister + { + RegisterType = RegisterType.Resource, Index = 1, SymbolicName = "_Tex", + }; + HlslFunctionNode function = new HlslFunctionNode { Name = "frag", Stage = HlslShaderStage.Fragment }; + function.Statements.Statements.Add(new HlslAssignmentStatement + { + Destination = Temp(0, 15), + Expression = new IRExpression.TextureOperationExpression + { + Operation = IRExpression.TextureOperation.Load, + Resource = resource, + Coordinates = new IRExpression.ConstantExpression + { + Kind = IRExpression.ConstantExpression.ConstantKind.Int, + RawValues = new uint[] { 0, 0, 0 }, + }, + }, + }); + pass.FragmentFunction = function; + subShader.Passes.Add(pass); shader.SubShaders.Add(subShader); - return HlslPrettyPrinter.Print(shader, fuseTemps: false); + string source = HlslPrettyPrinter.Print(shader, fuseTemps: false); + Assert.Contains("Texture2D _Tex : register(t0);", source); + Assert.Contains("Texture2D _Tex_t1 : register(t1);", source); + Assert.Contains("_Tex_t1.Load", source); } // Creates a temporary register with the requested index and lane mask. @@ -55,12 +82,6 @@ private static IRRegister Temp(uint index, byte mask = 1) return register; } - // Creates a scalar floating-point constant for the fixture. - private static IRExpression.ConstantExpression Float(float value) => new() - { - RawValues = new[] { unchecked((uint)BitConverter.SingleToInt32Bits(value)) }, - }; - // Builds a shader that reads raw storage from a partially described buffer. private static HlslShaderNode CreatePartialShader() { From 8ee93073072459eb234cfa24284c4966c276a2d8 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 18:31:36 +0100 Subject: [PATCH 85/93] Make fail-on-error stop on decompilation failures --- README.md | 4 ++- src/UnityShaderDecompiler.Cli/Program.cs | 15 ++++++++++- src/UnityShaderDecompiler.Core/Decompiler.cs | 27 ++++++++++++++----- ...XFailureTests.cs => FailurePolicyTests.cs} | 17 +++++++----- 4 files changed, 49 insertions(+), 14 deletions(-) rename tests/UnityShaderDecompiler.Tests/{DirectXFailureTests.cs => FailurePolicyTests.cs} (79%) diff --git a/README.md b/README.md index 0c7526f..4b79eda 100644 --- a/README.md +++ b/README.md @@ -176,7 +176,7 @@ and spaces are stripped). | `--out-root ` | Write `.shader` output and subprogram artifacts to `` instead of the project-local `Output/` | | `--dump-stages` | Print a full IR snapshot after every pipeline phase and every optimization pass | | `--dump-keywords` | Print each subprogram's global and local keyword sets | -| `--fail-on-error` | Abort on DirectX subprogram errors instead of logging and continuing (default: off) | +| `--fail-on-error` | Stop with a non-zero exit code on subprogram errors, invalid SSA, missing input, or a pass group with no renderable variant (default: off) | | `--save-subprograms` | Also write per-subprogram `program{i}.bin`, `program{i}.dxbc`, and `program{i}.hlsl` (disassembly) | | `--no-fuse-temps` | Disable temp fusion; keep every SSA copy as a separate statement | | `--no-surface-shaders` | Disable surface-shader recognition; emit raw reconstructed HLSLPROGRAM passes | @@ -190,6 +190,8 @@ and spaces are stripped). | `--recompile-verify-all [dir]` | Aggregate recompile verification over every decompiled `.shader` in `dir` | | `--disasm [dir]` | Dump DXBC disassembly for every non-compute subprogram into sibling `.disasm.txt` files | +`--fail-on-error` is strict: an export containing unsupported non-DXBC programs also fails when the parser cannot read them. + ## How it works diff --git a/src/UnityShaderDecompiler.Cli/Program.cs b/src/UnityShaderDecompiler.Cli/Program.cs index a1d26f0..e20000b 100644 --- a/src/UnityShaderDecompiler.Cli/Program.cs +++ b/src/UnityShaderDecompiler.Cli/Program.cs @@ -93,6 +93,7 @@ static void Main(string[] args) // resolved from either the working directory or one level up so the // app works whether it is launched from the repo root or from src/. string inputPath = args.Length > 0 ? args[0] : DefaultInputFolder(); + bool failOnError = args.Contains("--fail-on-error"); // Output root for the generated .shader (+ opt-in program*.bin/ // .dxbc/.hlsl artifacts). Defaults to the project-local Output/ so @@ -118,6 +119,10 @@ static void Main(string[] args) "`python Extract.py shaders_json/.json --out-dir Output " + "(--flat for a single root-level shader)` first, or pass the " + "path to a folder/blob explicitly."); + if (failOnError) + { + Environment.ExitCode = 1; + } return; } } @@ -132,6 +137,10 @@ static void Main(string[] args) Console.WriteLine( $"metadata.json not found next to {inputPath}. " + "Run Extract.py to produce a proper Stage 0 output folder."); + if (failOnError) + { + Environment.ExitCode = 1; + } return; } @@ -140,6 +149,10 @@ static void Main(string[] args) else { Console.WriteLine($"Input not found: {inputPath}"); + if (failOnError) + { + Environment.ExitCode = 1; + } return; } @@ -148,7 +161,7 @@ static void Main(string[] args) OutRoot = outRoot, DumpStages = args.Contains("--dump-stages"), DumpKeywords = args.Contains("--dump-keywords"), - FailOnError = args.Contains("--fail-on-error"), + FailOnError = failOnError, SaveSubprograms = args.Contains("--save-subprograms"), FuseTemps = !args.Contains("--no-fuse-temps"), ReconstructSurfaceShaders = !args.Contains("--no-surface-shaders"), diff --git a/src/UnityShaderDecompiler.Core/Decompiler.cs b/src/UnityShaderDecompiler.Core/Decompiler.cs index dccc644..89086ee 100644 --- a/src/UnityShaderDecompiler.Core/Decompiler.cs +++ b/src/UnityShaderDecompiler.Core/Decompiler.cs @@ -20,7 +20,7 @@ namespace UnityShaderDecompiler.Core; /// public sealed class DecompileOptions { - /// Abort on DirectX subprogram errors instead of logging and continuing. + /// Abort on decompilation errors instead of logging and continuing. public bool FailOnError { get; init; } /// Print the IR after every pipeline phase. @@ -248,6 +248,11 @@ public static DecompilationResult Decompile( if (!pipelineResult.SsaVerification.IsValid) { + if (options.FailOnError) + { + throw new InvalidDataException( + $"SSA verification failed for subprogram {i}: {string.Join("; ", pipelineResult.SsaVerification.Errors)}"); + } log.WriteLine("SSA verification FAILED:"); foreach (var error in pipelineResult.SsaVerification.Errors) log.WriteLine($" {error}"); @@ -344,11 +349,10 @@ public static DecompilationResult Decompile( } catch (Exception ex) { - if (options.FailOnError && sp.m_ProgramType is >= ShaderGpuProgramType.DX11VertexSM40 - and <= ShaderGpuProgramType.DX11DomainSM50) + if (options.FailOnError) { throw new InvalidDataException( - $"Failed to decompile DirectX subprogram {i} ({sp.m_ProgramType}): {ex.Message}", ex); + $"Failed to decompile subprogram {i} ({sp.m_ProgramType}): {ex.Message}", ex); } log.WriteLine($"No DXBC: {ex.Message}"); } @@ -361,7 +365,7 @@ public static DecompilationResult Decompile( // Stage 2b — recover the pass each candidate belongs to from the // serialization order and emit one pass per distinct compiled // (vertex, fragment) variant pair. - BuildVariantPasses(astShader, allPasses, candidates, log); + BuildVariantPasses(astShader, allPasses, candidates, log, options.FailOnError); log.WriteLine(); log.WriteLine("HLSL AST (Stage 2)"); @@ -471,10 +475,17 @@ private static void BuildVariantPasses( HlslShaderNode astShader, List allPasses, List candidates, - TextWriter log) + TextWriter log, + bool failOnError) { if (candidates.Count == 0) + { + if (failOnError && allPasses.Count > 0) + { + throw new InvalidDataException("No renderable subprograms were decompiled."); + } return; + } // Build texture property name mapping: texture properties (type=4) in // declaration order map to t-register slots (0, 1, 2, ...). This lets @@ -583,6 +594,10 @@ private static void BuildVariantPasses( if (pairs.Count == 0 && others.Count == 0) { + if (failOnError) + { + throw new InvalidDataException($"Pass group {gi} has no renderable variant."); + } log.WriteLine($" Pass group {gi}: {verts.Count} verts x {frags.Count} frags produced no renderable variant (skipped)."); } else diff --git a/tests/UnityShaderDecompiler.Tests/DirectXFailureTests.cs b/tests/UnityShaderDecompiler.Tests/FailurePolicyTests.cs similarity index 79% rename from tests/UnityShaderDecompiler.Tests/DirectXFailureTests.cs rename to tests/UnityShaderDecompiler.Tests/FailurePolicyTests.cs index 93cb691..4419731 100644 --- a/tests/UnityShaderDecompiler.Tests/DirectXFailureTests.cs +++ b/tests/UnityShaderDecompiler.Tests/FailurePolicyTests.cs @@ -4,16 +4,20 @@ namespace UnityShaderDecompiler.Tests; -// Covers opt-in DirectX failure handling. -public class DirectXFailureTests +// Covers opt-in subprogram failure handling across GPU formats. +public class FailurePolicyTests { - // Verifies that malformed DirectX program honors explicit failure policy. + // Verifies that malformed programs honor the explicit failure policy. [Theory] [InlineData(ShaderGpuProgramType.DX11VertexSM40, true)] [InlineData(ShaderGpuProgramType.DX11PixelSM50, true)] + [InlineData(ShaderGpuProgramType.GLLegacy, true)] + [InlineData(ShaderGpuProgramType.SPIRV, true)] [InlineData(ShaderGpuProgramType.DX11VertexSM40, false)] [InlineData(ShaderGpuProgramType.DX11PixelSM50, false)] - public void MalformedDirectXProgramHonorsExplicitFailurePolicy(ShaderGpuProgramType stage, bool failOnError) + [InlineData(ShaderGpuProgramType.GLLegacy, false)] + [InlineData(ShaderGpuProgramType.SPIRV, false)] + public void MalformedProgramHonorsExplicitFailurePolicy(ShaderGpuProgramType stage, bool failOnError) { using TempDirectory folder = new(); WriteInvalidProgram(folder, stage); @@ -30,13 +34,13 @@ public void MalformedDirectXProgramHonorsExplicitFailurePolicy(ShaderGpuProgramT InvalidDataException error = Assert.Throws(() => Decompiler.DecompileFolder(folder.Path, new DecompileOptions { FailOnError = true, Verbose = false, OutRoot = output })); - Assert.Contains("DirectX subprogram 0", error.Message); + Assert.Contains("subprogram 0", error.Message); Assert.Contains(stage.ToString(), error.Message); Assert.NotNull(error.InnerException); Assert.Empty(Directory.GetFiles(output, "*.shader")); } - // Writes a deliberately truncated DirectX program for failure-policy checks. + // Writes a deliberately truncated program for failure-policy checks. private static void WriteInvalidProgram(TempDirectory folder, ShaderGpuProgramType stage) { using MemoryStream payload = new(); @@ -63,3 +67,4 @@ private static void WriteInvalidProgram(TempDirectory folder, ShaderGpuProgramTy "{\"name\":\"Test/InvalidDxbc\",\"subShaders\":[]}"); } } + From db17cbb20f122f21cfac9c003da8bcfc30d0b42b Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 18:45:32 +0100 Subject: [PATCH 86/93] Remove redundant fail-on-error note --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 4b79eda..27ca9d2 100644 --- a/README.md +++ b/README.md @@ -190,8 +190,6 @@ and spaces are stripped). | `--recompile-verify-all [dir]` | Aggregate recompile verification over every decompiled `.shader` in `dir` | | `--disasm [dir]` | Dump DXBC disassembly for every non-compute subprogram into sibling `.disasm.txt` files | -`--fail-on-error` is strict: an export containing unsupported non-DXBC programs also fails when the parser cannot read them. - ## How it works From 8924576c76af045b291c3bf8e62cc75e2793a933 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 19:25:05 +0100 Subject: [PATCH 87/93] Regenerate golden shader after PR integration --- samples/HOLO_Holo.shader | 217 ++++++++---------- .../Core/Hlsl/HlslPrettyPrinter.cs | 1 + 2 files changed, 99 insertions(+), 119 deletions(-) diff --git a/samples/HOLO_Holo.shader b/samples/HOLO_Holo.shader index c057b98..5c73aec 100644 --- a/samples/HOLO_Holo.shader +++ b/samples/HOLO_Holo.shader @@ -45,29 +45,7 @@ Shader "HOLO/Holo" ZWrite On Blend SrcAlpha OneMinusSrcColor HLSLPROGRAM - cbuffer _GlobalsCB_b0 - { - float4 _Color; - float4 _diff_Color; - float4 _Diffuse_ST; - float4 _N_map_ST; - float4 _M_map_ST; - float _intensity; - float _deform; - float _Bias; - float _Scale; - float _Power; - float _Speed; - float _t; - float _X; - float _Y; - float _glitchColor; - float _monochrom; - float _Opacity; - float _noise_details; - float _OriginalUVSwitch; - float4 cb0_values[8]; - }; + cbuffer _GlobalsCB_b0 : register(b0) { float4 cb0_values[12]; }; cbuffer _UnityPerCameraCB_b1 { float4 _Time : packoffset(c0); @@ -86,6 +64,7 @@ Shader "HOLO/Holo" SamplerState sampler_MainTex; Texture2D _MainTex; SamplerState sampler_Diffuse; + SamplerState sampler_MainTex2; SamplerState sampler_originalDiffuse; Texture2D _originalDiffuse; Texture2D _Diffuse; @@ -126,32 +105,32 @@ Shader "HOLO/Holo" program1Output vert(program1Input i) { program1Output o = (program1Output)0; - float4x4 objectToView = mul(unity_ObjectToWorld, unity_MatrixV); - float r0_w_1 = (_Speed * _Time.x); + float4x4 objectToView0_r = mul(unity_ObjectToWorld, unity_MatrixV); + float r0_w_1 = (cb0_values[8].y * _Time.x); float r0_w_2 = (r0_w_1 * 60); float r0_w_3 = (sin(r0_w_2) + 1); - float r1_w_1 = (cos(r0_w_2) * _M_map_ST.y); - float r0_w_4 = (r0_w_3 * _M_map_ST.w); - float4 uvMMap_xyzw_2 = mad(i.texcoord0.xyxx, r1_w_1.xxxx, r0_w_4.xxxx); - float uvMMap_x_2 = uvMMap_xyzw_2.x; - float uvMMap_y_1 = uvMMap_xyzw_2.y; - float4 r4_xyzw_3 = _MainTex.SampleLevel(sampler_MainTex, (float4(uvMMap_x_2, uvMMap_y_1, uvMMap_x_2, uvMMap_x_2)).xy, 0); - float r0_w_5 = (int)(_noise_details); + float r1_w_1 = (cos(r0_w_2) * cb0_values[6].y); + float r0_w_4 = (r0_w_3 * cb0_values[6].w); + float4 uvMMap_r4_xyzw_2 = mad(i.texcoord0.xyxx, r1_w_1.xxxx, r0_w_4.xxxx); + float uvMMap_r4_x_2 = uvMMap_r4_xyzw_2.x; + float uvMMap_r4_y_1 = uvMMap_r4_xyzw_2.y; + float4 r4_xyzw_3 = _MainTex.SampleLevel(sampler_MainTex, (float4(uvMMap_r4_x_2, uvMMap_r4_y_1, uvMMap_r4_x_2, uvMMap_r4_x_2)).xy, 0); + float r0_w_5 = (int)(cb0_values[10].x); float r4_y_3 = ((_Time.xxyz * float4(0, 0.1, 0.1, 0.1))).y; float4 r4_xyzw_2 = (_Time.xxyz * float4(0, 0.1, 0.1, 0.1)); float r4_z_2 = r4_xyzw_2.z; float r4_w_2 = r4_xyzw_2.w; - float TEXCOORD0_x_2 = (i.texcoord0.xyxx).x; - float TEXCOORD0_y_1 = (i.texcoord0.xyxx).y; + float TEXCOORD0_r5_x_2 = (i.texcoord0.xyxx).x; + float TEXCOORD0_r5_y_1 = (i.texcoord0.xyxx).y; float r5_z_1 = 0; float r1_w_2 = r0_w_5; - float3 noiseAccum_xyz_2 = (float4(0, 0, 0, 0)).xyz; - float2 r5_yz_2 = float2(TEXCOORD0_y_1, r5_z_1); + float3 noiseAccum_r6_xyz_2 = (float4(0, 0, 0, 0)).xyz; + float2 r5_yz_2 = float2(TEXCOORD0_r5_y_1, r5_z_1); float r1_w_3 = r1_w_2; - float r5_x_3 = TEXCOORD0_x_2; + float r5_x_3 = TEXCOORD0_r5_x_2; float r2_w_1; float3 r7_xyz_1; - float3 noiseAccum_xyz_6; + float3 noiseAccum_r6_xyz_6; float r5_x_8; float r5_y_7; float r5_z_7; @@ -162,13 +141,13 @@ Shader "HOLO/Holo" r2_w_1 = (0 >= r1_w_3); if (r2_w_1) break; r7_xyz_1 = (floor(float4(r5_x_3, r5_yz_2.x, r5_yz_2.y, r5_x_3))).xyz; - noiseAccum_xyz_6 = ((noiseAccum_xyz_2.xyzx + (sin((mad(r7_xyz_1.xxzx, r7_xyz_1.yyyy, ((float4(r4_y_3, r4_z_2, r4_w_2, r4_y_3) * r7_xyz_1.xyzx)).xyzx)).xyzx)).xyzx)).xyz; + noiseAccum_r6_xyz_6 = ((noiseAccum_r6_xyz_2.xyzx + (sin((mad(r7_xyz_1.xxzx, r7_xyz_1.yyyy, ((float4(r4_y_3, r4_z_2, r4_w_2, r4_y_3) * r7_xyz_1.xyzx)).xyzx)).xyzx)).xyzx)).xyz; float4 r5_xyzw_8 = (float4(r5_x_3, r5_yz_2.x, r5_yz_2.y, r5_x_3) * float4(2.5, 2.5, 2.5, 0)); r5_x_8 = r5_xyzw_8.x; r5_y_7 = r5_xyzw_8.y; r5_z_7 = r5_xyzw_8.z; r1_w_4 = (r1_w_3 + -1); - noiseAccum_xyz_2 = noiseAccum_xyz_6; + noiseAccum_r6_xyz_2 = noiseAccum_r6_xyz_6; r5_yz_2 = float2(r5_y_7, r5_z_7); r1_w_3 = r1_w_4; r5_x_3 = r5_x_8; @@ -176,7 +155,7 @@ Shader "HOLO/Holo" float4 r4_xyzw_4 = (cb0_values[7].yyxy * float4(0, 0.001, 0.1, 0)); float r4_y_4 = r4_xyzw_4.y; float r4_z_3 = r4_xyzw_4.z; - float4 r5_xyzw_4 = (r4_y_4.xxxx * noiseAccum_xyz_2.xyzx); + float4 r5_xyzw_4 = (r4_y_4.xxxx * noiseAccum_r6_xyz_2.xyzx); float r5_x_4 = r5_xyzw_4.x; float r5_y_3 = r5_xyzw_4.y; float r5_z_3 = r5_xyzw_4.z; @@ -188,8 +167,8 @@ Shader "HOLO/Holo" float4 r4_xyzw_6 = (r4_xyzw_3.xxxx * float4(r4_y_5, r4_z_4, r4_w_3, r4_y_5)); float r4_y_6 = r4_xyzw_6.y; float r4_z_5 = r4_xyzw_6.z; - float r4_x_5 = (r4_x_4 * _X); - float r4_y_7 = (r4_y_6 * _Y); + float r4_x_5 = (r4_x_4 * cb0_values[8].w); + float r4_y_7 = (r4_y_6 * cb0_values[9].x); float r4_x_6 = ((float4(r4_x_5, r4_y_7, r4_z_5, r4_x_5) * i.normal0.xyzx)).x; float4 r4_xyzw_8 = (float4(r4_x_5, r4_y_7, r4_z_5, r4_x_5) * i.normal0.xyzx); float r4_y_8 = r4_xyzw_8.y; @@ -210,87 +189,87 @@ Shader "HOLO/Holo" float4 r6_xyzw_4 = mad(unity_MatrixVP[0], r5_xyzw_7.xxxx, r6_xyzw_3); float4 r6_xyzw_5 = mad(unity_MatrixVP[2], r5_xyzw_7.zzzz, r6_xyzw_4); o.sv_Position0.xyzw = mad(unity_MatrixVP[3], r5_xyzw_7.wwww, r6_xyzw_5); - float4 worldPos_xyzw_9 = mad(unity_ObjectToWorld[3].xyzx, i.position0.wwww, float4(r4_x_8, r4_y_10, r4_z_8, r4_x_8)); - float worldPos_x_9 = worldPos_xyzw_9.x; - float worldPos_y_11 = worldPos_xyzw_9.y; - float worldPos_z_9 = worldPos_xyzw_9.z; - float3 viewNormal_xyz_5 = (mad(objectToView[0].xyz.xyzx, i.normal0.xxxx, ((objectToView[1].xyz.xyzx * i.normal0.yyyy)).xyzx)).xyz; - float3 viewNormal_xyz_6 = (mad(objectToView[2].xyz.xyzx, i.normal0.zzzz, viewNormal_xyz_5.xyzx)).xyz; - float3 viewNormal_xyz_7 = (mad(objectToView[3].xyz.xyzx, i.normal0.wwww, viewNormal_xyz_6.xyzx)).xyz; - float r0_z_8 = dot(viewNormal_xyz_7.xyz, viewNormal_xyz_7.xyz); + float4 worldPos_r4_xyzw_9 = mad(unity_ObjectToWorld[3].xyzx, i.position0.wwww, float4(r4_x_8, r4_y_10, r4_z_8, r4_x_8)); + float worldPos_r4_x_9 = worldPos_r4_xyzw_9.x; + float worldPos_r4_y_11 = worldPos_r4_xyzw_9.y; + float worldPos_r4_z_9 = worldPos_r4_xyzw_9.z; + float3 viewNormal_r0_xyz_5 = (mad(objectToView0_r[0].xyz.xyzx, i.normal0.xxxx, ((objectToView0_r[1].xyz.xyzx * i.normal0.yyyy)).xyzx)).xyz; + float3 viewNormal_r0_xyz_6 = (mad(objectToView0_r[2].xyz.xyzx, i.normal0.zzzz, viewNormal_r0_xyz_5.xyzx)).xyz; + float3 viewNormal_r0_xyz_7 = (mad(objectToView0_r[3].xyz.xyzx, i.normal0.wwww, viewNormal_r0_xyz_6.xyzx)).xyz; + float r0_z_8 = dot(viewNormal_r0_xyz_7.xyz, viewNormal_r0_xyz_7.xyz); float r0_z_9 = rsqrt(r0_z_8); - float2 unitViewNormal_xy_8 = ((r0_z_9.xxxx * viewNormal_xyz_7.xyxx)).xy; - float worldNormal_x_6 = dot(i.normal0.xyz, unity_WorldToObject[0].xyz); - float worldNormal_y_6 = dot(i.normal0.xyz, unity_WorldToObject[1].xyz); - float worldNormal_z_6 = dot(i.normal0.xyz, unity_WorldToObject[2].xyz); - float r0_z_10 = dot((float4(worldNormal_x_6, worldNormal_y_6, worldNormal_z_6, worldNormal_x_6)).xyz, (float4(worldNormal_x_6, worldNormal_y_6, worldNormal_z_6, worldNormal_x_6)).xyz); + float2 unitViewNormal_r0_xy_8 = ((r0_z_9.xxxx * viewNormal_r0_xyz_7.xyxx)).xy; + float worldNormal_r1_x_6 = dot(i.normal0.xyz, unity_WorldToObject[0].xyz); + float worldNormal_r1_y_6 = dot(i.normal0.xyz, unity_WorldToObject[1].xyz); + float worldNormal_r1_z_6 = dot(i.normal0.xyz, unity_WorldToObject[2].xyz); + float r0_z_10 = dot((float4(worldNormal_r1_x_6, worldNormal_r1_y_6, worldNormal_r1_z_6, worldNormal_r1_x_6)).xyz, (float4(worldNormal_r1_x_6, worldNormal_r1_y_6, worldNormal_r1_z_6, worldNormal_r1_x_6)).xyz); float r0_z_11 = rsqrt(r0_z_10); - float3 unitWorldNormal_xyz_7 = ((r0_z_11.xxxx * float4(worldNormal_x_6, worldNormal_y_6, worldNormal_z_6, worldNormal_x_6))).xyz; - o.texcoord2.xy = (mad(unitViewNormal_xy_8.xyxx, float4(0.5, 0.5, 0, 0), float4(0.5, 0.5, 0, 0))).xy; - float4 dirToSurface_xyzw_9 = (float4(worldPos_x_9, worldPos_y_11, worldPos_z_9, worldPos_x_9) + -_WorldSpaceCameraPos.xyzx); - float dirToSurface_x_9 = dirToSurface_xyzw_9.x; - float dirToSurface_y_9 = dirToSurface_xyzw_9.y; - float dirToSurface_z_12 = dirToSurface_xyzw_9.z; - float r0_w_6 = dot((float4(dirToSurface_x_9, dirToSurface_y_9, dirToSurface_z_12, dirToSurface_x_9)).xyz, (float4(dirToSurface_x_9, dirToSurface_y_9, dirToSurface_z_12, dirToSurface_x_9)).xyz); + float3 unitWorldNormal_r1_xyz_7 = ((r0_z_11.xxxx * float4(worldNormal_r1_x_6, worldNormal_r1_y_6, worldNormal_r1_z_6, worldNormal_r1_x_6))).xyz; + o.texcoord2.xy = (mad(unitViewNormal_r0_xy_8.xyxx, float4(0.5, 0.5, 0, 0), float4(0.5, 0.5, 0, 0))).xy; + float4 dirToSurface_r0_xyzw_9 = (float4(worldPos_r4_x_9, worldPos_r4_y_11, worldPos_r4_z_9, worldPos_r4_x_9) + -_WorldSpaceCameraPos.xyzx); + float dirToSurface_r0_x_9 = dirToSurface_r0_xyzw_9.x; + float dirToSurface_r0_y_9 = dirToSurface_r0_xyzw_9.y; + float dirToSurface_r0_z_12 = dirToSurface_r0_xyzw_9.z; + float r0_w_6 = dot((float4(dirToSurface_r0_x_9, dirToSurface_r0_y_9, dirToSurface_r0_z_12, dirToSurface_r0_x_9)).xyz, (float4(dirToSurface_r0_x_9, dirToSurface_r0_y_9, dirToSurface_r0_z_12, dirToSurface_r0_x_9)).xyz); float r0_w_7 = rsqrt(r0_w_6); - float4 unitDirToSurface_xyzw_10 = (r0_w_7.xxxx * float4(dirToSurface_x_9, dirToSurface_y_9, dirToSurface_z_12, dirToSurface_x_9)); - float unitDirToSurface_x_10 = unitDirToSurface_xyzw_10.x; - float unitDirToSurface_y_10 = unitDirToSurface_xyzw_10.y; - float unitDirToSurface_z_13 = unitDirToSurface_xyzw_10.z; - float nDotV_x_11 = dot((float4(unitDirToSurface_x_10, unitDirToSurface_y_10, unitDirToSurface_z_13, unitDirToSurface_x_10)).xyz, unitWorldNormal_xyz_7.xyz); - float r0_y_11 = (_Scale * _Bias); - float fresnel_x_15 = pow((nDotV_x_11 + _t), _Power); - o.texcoord4.x = (fresnel_x_15 * r0_y_11); + float4 unitDirToSurface_r0_xyzw_10 = (r0_w_7.xxxx * float4(dirToSurface_r0_x_9, dirToSurface_r0_y_9, dirToSurface_r0_z_12, dirToSurface_r0_x_9)); + float unitDirToSurface_r0_x_10 = unitDirToSurface_r0_xyzw_10.x; + float unitDirToSurface_r0_y_10 = unitDirToSurface_r0_xyzw_10.y; + float unitDirToSurface_r0_z_13 = unitDirToSurface_r0_xyzw_10.z; + float nDotV_r0_x_11 = dot((float4(unitDirToSurface_r0_x_10, unitDirToSurface_r0_y_10, unitDirToSurface_r0_z_13, unitDirToSurface_r0_x_10)).xyz, unitWorldNormal_r1_xyz_7.xyz); + float r0_y_11 = (cb0_values[7].w * cb0_values[7].z); + float fresnel_r0_x_15 = pow((nDotV_r0_x_11 + cb0_values[8].z), cb0_values[8].x); + o.texcoord4.x = (fresnel_r0_x_15 * r0_y_11); o.texcoord0.xyzw = i.texcoord0.xyzw; o.texcoord1.xyzw = float4(0, 0, 0, 0); o.texcoord4.yzw = (float4(0, 0, 0, 0)).yzw; o.normal0.xyzw = float4(0, 0, 0, 0); o.texcoord2.z = 0; - o.texcoord3.xyz = unitWorldNormal_xyz_7.xyz; + o.texcoord3.xyz = unitWorldNormal_r1_xyz_7.xyz; return o; } #pragma fragment frag program3Output frag(program3Input i) { program3Output o = (program3Output)0; - float4 r0_xyzw_2 = (((_Speed * _Time.x)).xxxx * float4(30, 60, 120, 0)); + float4 r0_xyzw_2 = (((cb0_values[8].y * _Time.x)).xxxx * float4(30, 60, 120, 0)); float r0_x_2 = r0_xyzw_2.x; float r0_y_1 = r0_xyzw_2.y; float r0_z_1 = r0_xyzw_2.z; float r1_x_1 = sin(r0_y_1); float r0_y_2 = (r1_x_1 + 1); - float2 r1_yz_1 = ((r0_x_2.xxxx * _N_map_ST.zzwz)).yz; - float2 uvNMap_yz_2 = (mad(i.texcoord0.xxyx, _N_map_ST.xxyx, r1_yz_1.xxyx)).yz; - float4 sampleNMap_xyzw_1 = _MainTex.Sample(s2, (uvNMap_yz_2.xyxx).xy); - float r0_w_1 = (cos(r0_y_1) * _M_map_ST.y); - float r0_y_3 = (r0_y_2 * _M_map_ST.w); - float4 uvMMap_xyzw_4 = mad(i.texcoord0.xxxy, r0_w_1.xxxx, r0_y_3.xxxx); - float uvMMap_y_4 = uvMMap_xyzw_4.y; - float uvMMap_w_2 = uvMMap_xyzw_4.w; - float4 sampleMMap_xyzw_2 = _originalDiffuse.Sample(sampler_originalDiffuse, (float4(uvMMap_y_4, uvMMap_w_2, uvMMap_y_4, uvMMap_y_4)).xy); - float4 r0_xyzw_5 = (_OriginalUVSwitch.xxxx == float4(0, 0, 0, 1)); + float2 r1_yz_1 = ((r0_x_2.xxxx * cb0_values[5].zzwz)).yz; + float2 uvNMap_r1_yz_2 = (mad(i.texcoord0.xxyx, cb0_values[5].xxyx, r1_yz_1.xxyx)).yz; + float4 sampleNMap_r3_xyzw_1 = _MainTex.Sample(sampler_MainTex2, (uvNMap_r1_yz_2.xyxx).xy); + float r0_w_1 = (cos(r0_y_1) * cb0_values[6].y); + float r0_y_3 = (r0_y_2 * cb0_values[6].w); + float4 uvMMap_r0_xyzw_4 = mad(i.texcoord0.xxxy, r0_w_1.xxxx, r0_y_3.xxxx); + float uvMMap_r0_y_4 = uvMMap_r0_xyzw_4.y; + float uvMMap_r0_w_2 = uvMMap_r0_xyzw_4.w; + float4 sampleMMap_r2_xyzw_2 = _originalDiffuse.Sample(sampler_originalDiffuse, (float4(uvMMap_r0_y_4, uvMMap_r0_w_2, uvMMap_r0_y_4, uvMMap_r0_y_4)).xy); + float4 r0_xyzw_5 = (cb0_values[11].yyyy == float4(0, 0, 0, 1)); float r0_y_5 = r0_xyzw_5.y; float r0_w_3 = r0_xyzw_5.w; - float2 uvDiffuse_yz_3; - float4 sampleDiffuse_xyzw_2; - float4 sampleDiffuse_xyzw_3; + float2 uvDiffuse_r1_yz_3; + float4 sampleDiffuse_r4_xyzw_2; + float4 sampleDiffuse_r4_xyzw_3; if (r0_y_5) { - uvDiffuse_yz_3 = (mad(i.texcoord2.xxyx, _Diffuse_ST.yyyy, _Diffuse_ST.wwww)).yz; - sampleDiffuse_xyzw_2 = _Diffuse.Sample(sampler_Diffuse, (uvDiffuse_yz_3.xyxx).xy); - sampleDiffuse_xyzw_3 = sampleDiffuse_xyzw_2; + uvDiffuse_r1_yz_3 = (mad(i.texcoord2.xxyx, cb0_values[4].yyyy, cb0_values[4].wwww)).yz; + sampleDiffuse_r4_xyzw_2 = _Diffuse.Sample(sampler_Diffuse, (uvDiffuse_r1_yz_3.xyxx).xy); + sampleDiffuse_r4_xyzw_3 = sampleDiffuse_r4_xyzw_2; } else { - sampleDiffuse_xyzw_3 = float4(0, 0, 0, 0); + sampleDiffuse_r4_xyzw_3 = float4(0, 0, 0, 0); } - float4 sampleDiffuse_xyzw_5 = sampleDiffuse_xyzw_3; + float4 sampleDiffuse_r4_xyzw_5 = sampleDiffuse_r4_xyzw_3; if (r0_w_3) { float4 r4_xyzw_4 = _N_map.Sample(sampler_MainTex, (i.texcoord0.xyxx).xy); - sampleDiffuse_xyzw_5 = r4_xyzw_4; + sampleDiffuse_r4_xyzw_5 = r4_xyzw_4; } - float r0_y_6 = (sampleMMap_xyzw_2.x * sampleNMap_xyzw_1.x); + float r0_y_6 = (sampleMMap_r2_xyzw_2.x * sampleNMap_r3_xyzw_1.x); float4 r0_xyzw_3 = sin(float4(r0_x_2, r0_x_2, r0_z_1, r0_x_2)); float r0_x_3 = r0_xyzw_3.x; float r0_z_2 = r0_xyzw_3.z; @@ -298,8 +277,8 @@ Shader "HOLO/Holo" float r0_x_4 = r0_xyzw_4.x; float r0_z_3 = r0_xyzw_4.z; float r0_w_4 = (r0_x_4 * i.texcoord0.x); - float2 r1_yz_5 = (mad(r0_w_4.xxxx, _intensity.xxxx, i.texcoord0.xxyx)).yz; - float r0_w_5 = (int)(_noise_details); + float2 r1_yz_5 = (mad(r0_w_4.xxxx, cb0_values[7].xxxx, i.texcoord0.xxyx)).yz; + float r0_w_5 = (int)(cb0_values[10].x); float4 r2_xyzw_3 = (_Time.xyxx * float4(0.1, 0.1, 0, 0)); float r2_x_3 = r2_xyzw_3.x; float r2_y_2 = r2_xyzw_3.y; @@ -307,7 +286,7 @@ Shader "HOLO/Holo" float r1_w_1 = r0_w_5; float r1_w_2 = r1_w_1; float2 r2_zw_3 = r2_zw_2.xy; - float2 noiseAccum_xy_3 = (float4(0, 0, 0, 0)).xy; + float2 noiseAccum_r3_xy_3 = (float4(0, 0, 0, 0)).xy; float r3_z_3; float r3_z_4; float r3_w_3; @@ -315,7 +294,7 @@ Shader "HOLO/Holo" float r3_w_4; float r3_z_6; float r3_w_5; - float2 noiseAccum_xy_4; + float2 noiseAccum_r3_xy_4; float2 r2_zw_4; float r1_w_5; [loop] @@ -332,54 +311,54 @@ Shader "HOLO/Holo" float4 r3_xyzw_6 = sin(float4(r3_z_5, r3_z_5, r3_z_5, r3_w_4)); r3_z_6 = r3_xyzw_6.z; r3_w_5 = r3_xyzw_6.w; - noiseAccum_xy_4 = ((float4(r3_z_6, r3_w_5, r3_z_6, r3_z_6) + noiseAccum_xy_3.xyxx)).xy; + noiseAccum_r3_xy_4 = ((float4(r3_z_6, r3_w_5, r3_z_6, r3_z_6) + noiseAccum_r3_xy_3.xyxx)).xy; r2_zw_4 = ((r2_zw_3.xxxy * float4(0, 0, 2.5, 2.5))).zw; r1_w_5 = (r1_w_2 + -1); r1_w_2 = r1_w_5; r2_zw_3 = r2_zw_4.xy; - noiseAccum_xy_3 = noiseAccum_xy_4; + noiseAccum_r3_xy_3 = noiseAccum_r3_xy_4; } - float2 r1_yz_6 = ((noiseAccum_xy_3.xxyx * _glitchColor.xxxx)).yz; + float2 r1_yz_6 = ((noiseAccum_r3_xy_3.xxyx * cb0_values[9].yyyy)).yz; float r0_x_5 = ((float4(r0_x_4, r0_y_6, r0_z_3, r0_x_4) * float4(r1_yz_6.x, r1_x_1, r1_yz_6.x, r1_yz_6.x))).x; float4 r0_xyzw_7 = (float4(r0_x_4, r0_y_6, r0_z_3, r0_x_4) * float4(r1_yz_6.x, r1_x_1, r1_yz_6.x, r1_yz_6.x)); float r0_y_7 = r0_xyzw_7.y; float r0_z_4 = r0_xyzw_7.z; float r0_y_8 = (r1_yz_6.y * r0_y_7); - float4 r0_xyzw_6 = (float4(r0_x_5, r0_y_8, r0_z_4, r0_x_5) * _intensity.xxxx); + float4 r0_xyzw_6 = (float4(r0_x_5, r0_y_8, r0_z_4, r0_x_5) * cb0_values[7].xxxx); float r0_x_6 = r0_xyzw_6.x; float r0_y_9 = r0_xyzw_6.y; float r0_z_5 = r0_xyzw_6.z; - float r1_y_7 = mad(r0_y_9, 0.375, _Color.y); - float4 r1_xyzw_2 = mad(float4(r0_x_6, r0_x_6, r0_z_5, r0_x_6), float4(0.1875, 0, 0.75, 0), _Color.xxzx); + float r1_y_7 = mad(r0_y_9, 0.375, cb0_values[2].y); + float4 r1_xyzw_2 = mad(float4(r0_x_6, r0_x_6, r0_z_5, r0_x_6), float4(0.1875, 0, 0.75, 0), cb0_values[2].xxzx); float r1_x_2 = r1_xyzw_2.x; float r1_z_7 = r1_xyzw_2.z; - float r4_w_6 = (sampleDiffuse_xyzw_5.w * _Opacity); - float r0_x_7 = ((float4(sampleDiffuse_xyzw_5.x, sampleDiffuse_xyzw_5.y, sampleDiffuse_xyzw_5.z, r4_w_6) * _diff_Color)).x; - float4 r0_xyzw_10 = (float4(sampleDiffuse_xyzw_5.x, sampleDiffuse_xyzw_5.y, sampleDiffuse_xyzw_5.z, r4_w_6) * _diff_Color); + float r4_w_6 = (sampleDiffuse_r4_xyzw_5.w * cb0_values[9].w); + float r0_x_7 = ((float4(sampleDiffuse_r4_xyzw_5.x, sampleDiffuse_r4_xyzw_5.y, sampleDiffuse_r4_xyzw_5.z, r4_w_6) * cb0_values[3].xyzw)).x; + float4 r0_xyzw_10 = (float4(sampleDiffuse_r4_xyzw_5.x, sampleDiffuse_r4_xyzw_5.y, sampleDiffuse_r4_xyzw_5.z, r4_w_6) * cb0_values[3].xyzw); float r0_y_10 = r0_xyzw_10.y; float r0_z_6 = r0_xyzw_10.z; float r0_w_6 = r0_xyzw_10.w; - float r1_w_3 = (_monochrom == 1); + float r1_w_3 = (cb0_values[9].z == 1); float r2_x_5 = (r1_z_7 + (r1_y_7 + r1_x_2)); float r2_y_3 = (r0_y_10 + r0_x_7); - float r2_y_4 = mad(sampleDiffuse_xyzw_5.z, _diff_Color.z, r2_y_3); + float r2_y_4 = mad(sampleDiffuse_r4_xyzw_5.z, cb0_values[3].z, r2_y_3); float4 r2_xyzw_6 = (float4(r2_x_5, r2_y_4, r2_x_5, r2_x_5) * float4(0.33333334, 0.33333334, 0, 0)); float r2_x_6 = r2_xyzw_6.x; float r2_y_5 = r2_xyzw_6.y; - float4 monochromSelect_xyzw_8 = (r1_w_3.xxxx ? r2_y_5.xxxx : float4(r0_x_7, r0_y_10, r0_z_6, r0_x_7)); - float monochromSelect_x_8 = monochromSelect_xyzw_8.x; - float monochromSelect_y_11 = monochromSelect_xyzw_8.y; - float monochromSelect_z_7 = monochromSelect_xyzw_8.z; - float4 monochromSelect_xyzw_3 = (r1_w_3.xxxx ? r2_x_6.xxxx : float4(r1_x_2, r1_y_7, r1_z_7, r1_x_2)); - float monochromSelect_x_3 = monochromSelect_xyzw_3.x; - float monochromSelect_y_8 = monochromSelect_xyzw_3.y; - float monochromSelect_z_8 = monochromSelect_xyzw_3.z; - float4 r1_xyzw_4 = (-float4(monochromSelect_x_8, monochromSelect_y_11, monochromSelect_z_7, monochromSelect_x_8) + float4(monochromSelect_x_3, monochromSelect_y_8, monochromSelect_z_8, monochromSelect_x_3)); + float4 monochromSelect_r0_xyzw_8 = (r1_w_3.xxxx ? r2_y_5.xxxx : float4(r0_x_7, r0_y_10, r0_z_6, r0_x_7)); + float monochromSelect_r0_x_8 = monochromSelect_r0_xyzw_8.x; + float monochromSelect_r0_y_11 = monochromSelect_r0_xyzw_8.y; + float monochromSelect_r0_z_7 = monochromSelect_r0_xyzw_8.z; + float4 monochromSelect_r1_xyzw_3 = (r1_w_3.xxxx ? r2_x_6.xxxx : float4(r1_x_2, r1_y_7, r1_z_7, r1_x_2)); + float monochromSelect_r1_x_3 = monochromSelect_r1_xyzw_3.x; + float monochromSelect_r1_y_8 = monochromSelect_r1_xyzw_3.y; + float monochromSelect_r1_z_8 = monochromSelect_r1_xyzw_3.z; + float4 r1_xyzw_4 = (-float4(monochromSelect_r0_x_8, monochromSelect_r0_y_11, monochromSelect_r0_z_7, monochromSelect_r0_x_8) + float4(monochromSelect_r1_x_3, monochromSelect_r1_y_8, monochromSelect_r1_z_8, monochromSelect_r1_x_3)); float r1_x_4 = r1_xyzw_4.x; float r1_y_9 = r1_xyzw_4.y; float r1_z_9 = r1_xyzw_4.z; - float r1_w_4 = (-r0_w_6 + _Color.w); - o.sv_Target0.xyzw = mad(i.texcoord4.xxxx, float4(r1_x_4, r1_y_9, r1_z_9, r1_w_4), float4(monochromSelect_x_8, monochromSelect_y_11, monochromSelect_z_7, r0_w_6)); + float r1_w_4 = (-r0_w_6 + cb0_values[2].w); + o.sv_Target0.xyzw = mad(i.texcoord4.xxxx, float4(r1_x_4, r1_y_9, r1_z_9, r1_w_4), float4(monochromSelect_r0_x_8, monochromSelect_r0_y_11, monochromSelect_r0_z_7, r0_w_6)); return o; } ENDHLSL diff --git a/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslPrettyPrinter.cs b/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslPrettyPrinter.cs index db13fa2..0940b1d 100644 --- a/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslPrettyPrinter.cs +++ b/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslPrettyPrinter.cs @@ -235,6 +235,7 @@ private static void PrintPassProgram(StringBuilder sb, HlslPassNode pass, bool f // Shares one name for variables at the same constant-buffer offset. Dictionary cbufferRenames = BuildCbufferRenameMap(pass); + EnsureUniqueTextureNames(pass.Resources); PrintPassDeclarations(sb, pass, stripRegisterBindings, cbufferRenames); if (pass.VertexFunction is not null) { From f5040e9745d6999aaf7a2b86d831ecfaf5d3135f Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Wed, 4 Nov 2026 19:31:35 +0100 Subject: [PATCH 88/93] Reuse shared vertex across metadata passes --- src/UnityShaderDecompiler.Core/Decompiler.cs | 22 +++++++++++++++++-- .../DecompilerIntegrationTests.cs | 12 ++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/UnityShaderDecompiler.Core/Decompiler.cs b/src/UnityShaderDecompiler.Core/Decompiler.cs index d3ff6b4..1abedc6 100644 --- a/src/UnityShaderDecompiler.Core/Decompiler.cs +++ b/src/UnityShaderDecompiler.Core/Decompiler.cs @@ -568,12 +568,30 @@ private static void BuildVariantPasses( } } + // Reuse a serialized vertex when each metadata pass has its own fragment. + bool sharedVertexPasses = groups.Count == 1 && verts.Count == 1 + && frags.Count == allPasses.Count && frags.Count > 1 + && frags.All(f => verts[0].OutputInterpolators == f.InputInterpolators + && verts[0].OutputSignature.Split(';', StringSplitOptions.RemoveEmptyEntries) + .All(f.InputSignature.Split(';', StringSplitOptions.RemoveEmptyEntries).Contains)); + if (sharedVertexPasses) + { + foreach (DecompiledSubprogram fragment in frags) + { + if (usedFrags.Add(fragment)) + { + pairs.Add((verts[0], fragment)); + } + } + } + // First pair fills the shell pass; the rest get clones. for (int pi = 0; pi < pairs.Count; pi++) { var (v, f) = pairs[pi]; - HlslPassNode pass = pi == 0 ? shell : ClonePass(shell); - if (!ReferenceEquals(pass, shell)) + HlslPassNode pass = sharedVertexPasses ? allPasses[pi] + : pi == 0 ? shell : ClonePass(shell); + if (!sharedVertexPasses && !ReferenceEquals(pass, shell)) { HlslSubShaderNode ss = subshaderByPass[shell]; int insertAt = ss.Passes.IndexOf(shell) + 1; diff --git a/tests/UnityShaderDecompiler.Tests/DecompilerIntegrationTests.cs b/tests/UnityShaderDecompiler.Tests/DecompilerIntegrationTests.cs index e3018c3..53ba7a7 100644 --- a/tests/UnityShaderDecompiler.Tests/DecompilerIntegrationTests.cs +++ b/tests/UnityShaderDecompiler.Tests/DecompilerIntegrationTests.cs @@ -16,6 +16,18 @@ namespace UnityShaderDecompiler.Tests; /// public class DecompilerIntegrationTests { + // Keeps both metadata passes when they share one compiled vertex program. + [Fact] + public void BrightPassFilter_ReusesVertexAcrossFragmentPasses() + { + DecompilationResult result = FixtureCache.Decompile("Hidden_BrightPassFilter2"); + + Assert.Equal(2, result.Passes.Count); + Assert.All(result.Passes, pass => Assert.NotNull(pass.VertexFunction)); + Assert.All(result.Passes, pass => Assert.NotNull(pass.FragmentFunction)); + Assert.DoesNotContain("_StubCB", result.ShaderText); + } + public static TheoryData Fixtures => new() { { "HOLO_Holo", "HOLO/Holo" }, From 2f4a680fb27d813a9556752c6771507205440b01 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Thu, 5 Nov 2026 14:20:03 +0100 Subject: [PATCH 89/93] Reject guessed resource bindings and incompatible buffer layouts Preserve named constants and require verified resource identities instead of raw replacement arrays or property-order bindings. Nine focused tests pass on this branch; the combined checkout passes 16 focused tests but retains 28 full-suite failures. Complete parameter extraction and Unity rendering validation remain required. --- README.md | 7 +- .../Core/Hlsl/HlslAST.cs | 3 +- .../Core/Hlsl/HlslAstBuilder.cs | 108 ++-------- .../Core/Hlsl/HlslPrettyPrinter.cs | 43 +--- .../Core/Hlsl/VerifiedResourceBindings.cs | 98 +++++++++ src/UnityShaderDecompiler.Core/Decompiler.cs | 87 +------- .../CbufferEmissionTests.cs | 32 +-- .../ResourceBindingLayoutsTests.cs | 191 ++++++++---------- 8 files changed, 216 insertions(+), 353 deletions(-) create mode 100644 src/UnityShaderDecompiler.Core/Core/Hlsl/VerifiedResourceBindings.cs diff --git a/README.md b/README.md index 47412cc..76672ee 100644 --- a/README.md +++ b/README.md @@ -236,8 +236,11 @@ Each subprogram goes through `IRPipeline` (`DXBC/IR/IRPipeline.cs`): the matrix, vector, texture, loop, and shader-pattern recognizers. 9. **Leave SSA** — eliminate phis and coalesce copies. 10. **Metadata binding** — attach RDEF/ISGN/OSGN symbolic names to the IR. - Same-named resources keep distinct binding slots; raw constant-buffer fallback - storage preserves the serialized extent and starts at byte zero. + Resource names must come from reflection or binding metadata, never property + order. Missing names, unresolved constant-buffer reads, overlapping layouts + and incompatible stage bindings stop emission. Partial metadata cannot replace + named uniforms with raw arrays. Existing stripped fixtures require complete + parameter extraction before they can pass this stricter contract. 11. **AST build** — one function node per subprogram; the Shader/SubShader/Pass shell comes from `metadata.json`. 12. **Pass and variant recovery** — `BuildVariantPasses` groups subprograms diff --git a/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslAST.cs b/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslAST.cs index d658dfe..285071a 100644 --- a/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslAST.cs +++ b/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslAST.cs @@ -291,6 +291,7 @@ public sealed class HlslCBufferVariable public sealed class HlslResourceNode { public string Name { get; set; } = ""; + public string Stage { get; set; } = ""; public HlslResourceKind Kind { get; init; } public uint Slot { get; init; } @@ -322,4 +323,4 @@ public sealed class HlslFieldNode // float vs int vs uint) — left as a hint for Stage 8 to firm up, not // invented here. public string TypeHint { get; init; } = "float4"; -} \ No newline at end of file +} diff --git a/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslAstBuilder.cs b/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslAstBuilder.cs index c294ab8..b629b51 100644 --- a/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslAstBuilder.cs +++ b/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslAstBuilder.cs @@ -252,7 +252,7 @@ public static IEnumerable BuildResources( { var rdNode = new HlslResourceNode { - Name = cb.SymbolicName ?? $"cb{cb.Slot}", + Name = VerifiedResourceBindings.RequireName(cb.SymbolicName, "constant buffer", cb.Slot), Kind = HlslResourceKind.ConstantBuffer, Slot = cb.Slot, }; @@ -290,25 +290,25 @@ public static IEnumerable BuildResources( }; foreach (CbufferVariableMetadata v in meta.Variables) metaNode.Variables.Add(MakeCbufferVariable(v)); - AddCbufferFallback(metaNode, cb.Slot, maxCbufferSlot); + RequireResolvedCbuffer(cb.Slot, maxCbufferSlot); yield return metaNode; break; } var cbNode = new HlslResourceNode { - Name = cb.SymbolicName ?? $"cb{cb.Slot}", + Name = VerifiedResourceBindings.RequireName(cb.SymbolicName, "constant buffer", cb.Slot), Kind = HlslResourceKind.ConstantBuffer, Slot = cb.Slot, }; - AddCbufferFallback(cbNode, cb.Slot, maxCbufferSlot); + RequireResolvedCbuffer(cb.Slot, maxCbufferSlot); yield return cbNode; break; case IRDeclaration.IRResourceDeclaration res: string texName = (textureNames is not null && textureNames.TryGetValue(res.Slot, out string? pname)) ? pname - : res.SymbolicName ?? $"t{res.Slot}"; + : VerifiedResourceBindings.RequireName(res.SymbolicName, "texture", res.Slot); yield return new HlslResourceNode { Name = texName, @@ -320,23 +320,7 @@ public static IEnumerable BuildResources( case IRDeclaration.IRSamplerDeclaration samp: bool isComparison = ComparisonSamplers(blocks).Contains(samp.Slot); - // Unity pairs samplers with textures by naming convention: - // "sampler_" must match a Texture2D named - // "". Build the mapping from the IR's - // Sample() calls to find which texture each sampler serves. - string sampName; - if (textureNames is not null - && SamplerToTextureSlot(blocks, samp.Slot) is uint texSlot - && textureNames.TryGetValue(texSlot, out string? pairedTex)) - { - // Unity convention: "sampler_" where - // TextureName has no leading underscore — so - // Texture2D _MainTex pairs with SamplerState sampler_MainTex. - string stripped = pairedTex.StartsWith('_') ? pairedTex[1..] : pairedTex; - sampName = $"sampler_{stripped}"; - } - else - sampName = samp.SymbolicName ?? SamplerInlineName(samp.Slot, isComparison); + string sampName = VerifiedResourceBindings.RequireName(samp.SymbolicName, "sampler", samp.Slot); yield return new HlslResourceNode { Name = sampName, @@ -351,7 +335,7 @@ public static IEnumerable BuildResources( case IRDeclaration.IRUAVDeclaration uav: yield return new HlslResourceNode { - Name = uav.SymbolicName ?? $"u{uav.Slot}", + Name = VerifiedResourceBindings.RequireName(uav.SymbolicName, "UAV", uav.Slot), Kind = HlslResourceKind.Uav, Slot = uav.Slot, TypeHint = TextureTypeHint(uav.Dimension, uav: true), @@ -361,17 +345,7 @@ public static IEnumerable BuildResources( } } - // Scans the subprogram's IR for cbuffer reads like cb2[10].yyyy and - // returns, per cbuffer slot, the highest register index (element) used. - // Used to size the synthesized float4 array for RDEF-less cbuffers. - // - // `cbuffers`/`stage` are the ShaderLab-metadata layout for the pass - // (the substitute for the RDEF Unity strips from shipped bytecode). - // A read that the metadata resolves to a named member (HlslPrettyPrinter - // resolves the same way) never touches the synthesized cbN_values array, - // so it must not count toward the array's size — otherwise the fallback - // is declared bigger than needed, or declared at all for a slot whose - // every read resolves (a dead declaration). + // Finds unresolved reads that must stop reconstruction. private static Dictionary BuildMaxCbufferSlot( List? blocks, Dictionary<(int Slot, string Stage), CbufferMetadata>? cbuffers, @@ -388,23 +362,18 @@ private static Dictionary BuildMaxCbufferSlot( { foreach (IRRegister reg in stmt.Uses.Concat(stmt.Defines)) { - if (reg.RegisterType != RegisterType.ConstantBuffer || reg.Indices.Count < 2) + if (reg.RegisterType != RegisterType.ConstantBuffer || reg.Indices.Count < 2 || reg.SymbolicName is not null) continue; uint slot = reg.Indices[0]; uint elem = reg.Indices[1]; - // Dynamic reads (cb0[r2.x + 4]) can touch any element up - // to the real cbuffer size, which RDEF-less bytecode - // doesn't tell us — reserve a generous range for them. + // Dynamic reads require a resolved array layout. if (reg.RelativeIndices.Length > 1 && reg.RelativeIndices[1] is not null) { - elem = Math.Max(elem, 64); + throw new InvalidDataException($"Unresolved dynamic constant-buffer access at b{slot}; an exact array layout is required."); } - // Reads the ShaderLab metadata resolves to a named member - // (variable name + optional row/component) render as that - // identifier, never as cbN_values[slot], so they impose no - // requirement on the synthesized fallback array. + // Named reads use the serialized layout without replacement storage. else if (ResolvesToNamedMember(reg, cbuffers, stage)) { continue; @@ -464,41 +433,6 @@ or IRExpression.TextureOperation.SampleCompareLevelZero return result; } - // Returns the texture register slot used with a given sampler slot - // in the first Sample/SampleLevel/SampleBias/SampleGrad/Gather call - // found in the IR, or null if the sampler is never used. - private static uint? SamplerToTextureSlot(List? blocks, uint samplerSlot) - { - if (blocks is null) return null; - foreach (IRBlock block in blocks) - { - foreach (IRStatement stmt in block.Statements) - { - foreach (IRExpression expr in AllExpressions(stmt)) - { - if (expr is not IRExpression.TextureOperationExpression tex) - continue; - if (tex.Sampler is { RegisterType: RegisterType.Sampler } s && s.Index == samplerSlot - && tex.Resource is { RegisterType: RegisterType.Resource } r) - return r.Index; - } - } - } - return null; - } - - // Returns a Unity-recognised inline sampler name for the given slot. - // Unity's HLSL compiler rejects bare "SamplerState s0" — the name must - // contain filter + wrap modes (e.g. "sampler_linear_clamp"). - // We use a base name that is always valid HLSL; the slot number is - // appended only when needed for uniqueness. - private static string SamplerInlineName(uint slot, bool comparison) - { - if (comparison) - return slot == 0 ? "sampler_comparison_linear_clamp" : $"sampler_comparison_linear_clamp{slot}"; - return slot == 0 ? "sampler_linear_clamp" : $"sampler_linear_clamp{slot}"; - } - // Depth-first walk of every sub-expression in a statement (mirrors // IRExpressionRewriter's traversal so new node types stay in sync). private static IEnumerable AllExpressions(IRStatement stmt) @@ -635,23 +569,15 @@ private static string SanitizeIdentifier(string name) return new string(chars); } - // Safety net: Unity's per-buffer layout is "partial" (some members are - // omitted), so a few reads may not resolve to a named variable. Keep a - // synthesized float4 array covering every accessed slot so those reads - // still compile as cbN_values[slot]. - private static void AddCbufferFallback(HlslResourceNode node, uint slot, Dictionary maxCbufferSlot) + // Stops when a used register has no named parameter layout. + private static void RequireResolvedCbuffer(uint slot, Dictionary maxCbufferSlot) { if (!maxCbufferSlot.TryGetValue(slot, out uint maxSlot)) + { return; + } - node.Variables.Add(new HlslCBufferVariable - { - Name = $"cb{slot}_values", - TypeName = "float4", - Offset = 0, - Size = (maxSlot + 1) * 16, - ArraySize = (int)(maxSlot + 1), - }); + throw new InvalidDataException($"Unresolved constant-buffer access at b{slot}[{maxSlot}]; named parameter metadata is required."); } private static HlslStructNode BuildStruct(string name, IEnumerable<(uint Register, string? SymbolicName)> entries) diff --git a/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslPrettyPrinter.cs b/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslPrettyPrinter.cs index 714ec83..f7bc2eb 100644 --- a/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslPrettyPrinter.cs +++ b/src/UnityShaderDecompiler.Core/Core/Hlsl/HlslPrettyPrinter.cs @@ -196,7 +196,7 @@ private static void PrintPass(StringBuilder sb, HlslPassNode pass, bool fuseTemp // in the function body are redirected. var cbufferRenames = BuildCbufferRenameMap(pass); - EnsureUniqueTextureNames(pass.Resources); + VerifiedResourceBindings.ValidatePass(pass.Resources); foreach (HlslResourceNode res in pass.Resources) PrintResource(sb, res, stripRegisterBindings, cbufferRenames); @@ -274,27 +274,6 @@ private static void PrintRenderState(StringBuilder sb, HlslRenderState s) // ---------- Stage 7: Resources ---------- - // Gives textures on different slots distinct HLSL identifiers in one pass. - private static void EnsureUniqueTextureNames(List resources) - { - HashSet used = new HashSet(StringComparer.Ordinal); - foreach (HlslResourceNode resource in resources) - { - if (resource.Kind != HlslResourceKind.Texture || used.Add(resource.Name)) - { - continue; - } - string stem = $"{resource.Name}_t{resource.Slot}"; - string name = stem; - int suffix = 1; - while (!used.Add(name) || resources.Any(other => !ReferenceEquals(other, resource) && other.Name == name)) - { - name = $"{stem}_{suffix++}"; - } - resource.Name = name; - } - } - // When vert and frag have different variables at the same byte offset // within a merged cbuffer, one must be declared and the other's // references redirected. Returns a map from the dropped variable name @@ -376,13 +355,7 @@ bool IsMatrixName(string name) => private static void PrintResource(StringBuilder sb, HlslResourceNode res, bool stripRegisterBindings, Dictionary? cbufferRenames = null) { - if (res.Kind == HlslResourceKind.ConstantBuffer - && res.Variables.FirstOrDefault(v => v.Name == $"cb{res.Slot}_values" && v.ArraySize is not null) is { } raw) - { - sb.Append(" cbuffer ").Append(CbufferName(res)).Append(" : register(b").Append(res.Slot) - .Append(") { float4 ").Append(raw.Name).Append('[').Append(raw.ArraySize).Append("]; };\n"); - return; - } + VerifiedResourceBindings.Validate(res); // Unity standard cbuffers are assigned to per-stage slots by Unity's // compiler from the cbuffer NAME alone — an explicit register binding @@ -597,7 +570,6 @@ private sealed class PrintContext // used to render RDEF-less cbN[slot] reads as real variable names. public Dictionary Cbuffers { get; set; } = new(); - public HashSet RawCbufferSlots { get; } = new(); // Identifier each defining instruction actually declared for a // value, keyed by the value's structural identity (storage location @@ -692,11 +664,6 @@ private static void PrintFunction( if (resources is not null) foreach (HlslResourceNode res in resources) { - if (res.Kind == HlslResourceKind.ConstantBuffer - && res.Variables.Any(v => v.Name == $"cb{res.Slot}_values" && v.ArraySize is not null)) - { - ctx.RawCbufferSlots.Add((int)res.Slot); - } if (res.Kind == HlslResourceKind.Texture) { ctx.TextureTypeBySlot[(int)res.Slot] = res.TypeHint ?? "Texture2D"; @@ -1696,10 +1663,6 @@ private static string CbufferElementName(IRRegister reg, PrintContext ctx) // 3. synthesized cbN_values[slot] fallback array. private static string RenderCbufferRead(IRRegister reg, PrintContext ctx) { - if (reg.Indices.Count >= 2 && ctx.RawCbufferSlots.Contains((int)reg.Indices[0])) - { - return CbufferElementName(reg, ctx) + MaskOrSwizzleSuffix(reg); - } if (reg.SymbolicName is not null) { string name = reg.SymbolicName; @@ -1727,7 +1690,7 @@ private static string RenderCbufferRead(IRRegister reg, PrintContext ctx) return named; } - return CbufferElementName(reg, ctx) + MaskOrSwizzleSuffix(reg); + throw new InvalidDataException("Unresolved constant-buffer read; refusing anonymous register storage."); } // Maps a cbN[reg] register read (plus its component swizzle) onto a diff --git a/src/UnityShaderDecompiler.Core/Core/Hlsl/VerifiedResourceBindings.cs b/src/UnityShaderDecompiler.Core/Core/Hlsl/VerifiedResourceBindings.cs new file mode 100644 index 0000000..0f1e6a2 --- /dev/null +++ b/src/UnityShaderDecompiler.Core/Core/Hlsl/VerifiedResourceBindings.cs @@ -0,0 +1,98 @@ +using System.IO; +using UnityShaderDecompiler.Core.Hlsl.Ast; + +namespace UnityShaderDecompiler.Core.Hlsl; + +// Rejects missing identities and incompatible resource layouts before emission. +internal static class VerifiedResourceBindings +{ + // Requires a name recovered from reflection or serialized binding metadata. + internal static string RequireName(string? name, string kind, uint slot) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new InvalidDataException($"Missing {kind} binding name at slot {slot}; resource parameter metadata is required."); + } + return name; + } + + // Combines only resources whose identities and layouts agree across stages. + internal static void Attach(List resources, HlslResourceNode incoming, string stage) + { + incoming.Stage = stage; + Validate(incoming); + HlslResourceNode? existing = resources.FirstOrDefault(item => item.Kind == incoming.Kind && item.Slot == incoming.Slot); + HlslResourceNode? duplicate = resources.FirstOrDefault(item => item.Name == incoming.Name && item != existing); + if (duplicate is not null) + { + throw new InvalidDataException($"Resource {incoming.Name} has incompatible bindings in {duplicate.Stage} and {stage}."); + } + if (existing is null) + { + resources.Add(incoming); + return; + } + if (existing.Name != incoming.Name || existing.TypeHint != incoming.TypeHint) + { + throw new InvalidDataException($"Conflicting {incoming.Kind} slot {incoming.Slot}: {existing.Stage} {existing.Name}, {stage} {incoming.Name}."); + } + foreach (HlslCBufferVariable variable in incoming.Variables) + { + MergeVariable(existing, variable); + } + } + + // Requires named, non-overlapping members instead of replacement storage. + internal static void Validate(HlslResourceNode resource) + { + RequireName(resource.Name, resource.Kind.ToString(), resource.Slot); + if (resource.Kind != HlslResourceKind.ConstantBuffer) + { + return; + } + HlslResourceNode checkedLayout = new HlslResourceNode { Name = resource.Name }; + foreach (HlslCBufferVariable variable in resource.Variables) + { + RequireName(variable.Name, "constant-buffer member", resource.Slot); + if (string.IsNullOrWhiteSpace(variable.TypeName) || variable.Size == 0) + { + throw new InvalidDataException($"Incomplete member layout for {resource.Name}.{variable.Name}."); + } + MergeVariable(checkedLayout, variable); + } + } + + // Rejects duplicate names instead of inventing new runtime binding names. + internal static void ValidatePass(IEnumerable resources) + { + HashSet names = new HashSet(StringComparer.Ordinal); + foreach (HlslResourceNode resource in resources) + { + Validate(resource); + if (!names.Add(resource.Name)) + { + throw new InvalidDataException($"Duplicate resource name {resource.Name}; cannot invent a replacement binding."); + } + } + } + + // Preserves members only when their byte ranges and types are compatible. + private static void MergeVariable(HlslResourceNode resource, HlslCBufferVariable incoming) + { + foreach (HlslCBufferVariable existing in resource.Variables) + { + if (existing.Name == incoming.Name && existing.TypeName == incoming.TypeName && + existing.Offset == incoming.Offset && existing.Size == incoming.Size && existing.ArraySize == incoming.ArraySize) + { + return; + } + if (existing.Name == incoming.Name || + ((ulong)existing.Offset < (ulong)incoming.Offset + incoming.Size && + (ulong)incoming.Offset < (ulong)existing.Offset + existing.Size)) + { + throw new InvalidDataException($"Conflicting member layouts for {resource.Name}: {existing.Name} and {incoming.Name}."); + } + } + resource.Variables.Add(incoming); + } +} diff --git a/src/UnityShaderDecompiler.Core/Decompiler.cs b/src/UnityShaderDecompiler.Core/Decompiler.cs index 7c79334..7c7c558 100644 --- a/src/UnityShaderDecompiler.Core/Decompiler.cs +++ b/src/UnityShaderDecompiler.Core/Decompiler.cs @@ -5,6 +5,7 @@ using System.Security.Cryptography; using AssetStudio; using UnityShaderDecompiler.Core.Hlsl.Ast; +using UnityShaderDecompiler.Core.Hlsl; using UnityShaderDecompiler.Core.DXBC; using UnityShaderDecompiler.Core.DXBC.Chunks; using UnityShaderDecompiler.Core.DXBC.Extraction; @@ -467,16 +468,8 @@ private static void BuildVariantPasses( if (candidates.Count == 0) return; - // Build texture property name mapping: texture properties (type=4) in - // declaration order map to t-register slots (0, 1, 2, ...). This lets - // the decompiler emit "Texture2D _MainTex : register(t0)" instead of - // "Texture2D t0 : register(t0)" — Unity needs variable names matching - // the Properties block to bind textures at runtime. - var textureNames = new Dictionary(); - uint texSlot = 0; - foreach (HlslPropertyNode prop in astShader.Properties) - if (prop.Kind == HlslPropertyKind.Texture) - textureNames[texSlot++] = prop.Name; + // Property order does not identify compiled resource binding slots. + Dictionary textureNames = new Dictionary(); foreach (DecompiledSubprogram c in candidates) c.Hash = Convert.ToHexString(SHA256.HashData(c.DxbcBytes)); @@ -607,79 +600,9 @@ private static void AttachPassProgram( c.Stage.ToString(), textureNames).ToList(); - foreach (HlslResourceNode res in resources) + foreach (HlslResourceNode resource in resources) { - // Cbuffers merge by slot: vert and frag subprograms of the same - // pass touch the same constant-buffer slots, so union the - // members (keeping the largest array size for any fallback). - // Equal names do not make resources at different bindings identical. - HlslResourceNode? existing = pass.Resources.FirstOrDefault(r => - r.Kind == res.Kind && r.Slot == res.Slot); - - if (existing is null) - { - // Sampler name collision: two different sampler slots may both - // map to the same texture property (e.g. s0 and s2 both serve - // _MainTex). Append the slot index to make the name unique. - if (res.Kind == HlslResourceKind.Sampler - && pass.Resources.Any(r => r.Kind == HlslResourceKind.Sampler && r.Name == res.Name)) - { - res.Name = $"{res.Name}{res.Slot}"; - } - pass.Resources.Add(res); - } - else if (res.Kind == HlslResourceKind.ConstantBuffer) - { - // vert/frag subprograms of one pass can touch different - // slots of the same cbuffer, and each BuildResources call - // only sees its own blocks. Union the members, keeping the - // largest array size so the RDEF-less float4 fallback - // covers every subprogram's accesses. - foreach (HlslCBufferVariable v in res.Variables) - { - HlslCBufferVariable? match = existing.Variables.FirstOrDefault(x => x.Name == v.Name); - if (match is null) - { - existing.Variables.Add(v); - } - else if (v.ArraySize is { } n && (match.ArraySize is not { } m || n > m)) - { - existing.Variables.Remove(match); - existing.Variables.Add(v); - } - } - } - } - - // A fallback array is shared by all stages in the emitted HLSL pass. - // Once present, every stage reads raw slots, including stages whose - // reads were resolved to named metadata members before the merge. - foreach (HlslResourceNode buffer in pass.Resources.Where(r => r.Kind == HlslResourceKind.ConstantBuffer)) - { - HlslCBufferVariable? raw = buffer.Variables.FirstOrDefault( - v => v.Name == $"cb{buffer.Slot}_values" && v.ArraySize is not null); - if (raw is null) - { - continue; - } - int metadataRegisters = pass.Cbuffers - .Where(entry => entry.Key.Slot == (int)buffer.Slot) - .Select(entry => (entry.Value.Size + 15) / 16) - .DefaultIfEmpty(0) - .Max(); - int count = Math.Max(raw.ArraySize!.Value, metadataRegisters); - if (count > raw.ArraySize.Value) - { - buffer.Variables.Remove(raw); - buffer.Variables.Add(new HlslCBufferVariable - { - Name = raw.Name, - TypeName = raw.TypeName, - Offset = raw.Offset, - Size = (uint)count * 16, - ArraySize = count, - }); - } + VerifiedResourceBindings.Attach(pass.Resources, resource, c.Stage.ToString()); } if (c.Function.InputStruct is not null) pass.Structs.Add(c.Function.InputStruct); diff --git a/tests/UnityShaderDecompiler.Tests/CbufferEmissionTests.cs b/tests/UnityShaderDecompiler.Tests/CbufferEmissionTests.cs index fde28cd..5726e7e 100644 --- a/tests/UnityShaderDecompiler.Tests/CbufferEmissionTests.cs +++ b/tests/UnityShaderDecompiler.Tests/CbufferEmissionTests.cs @@ -11,34 +11,16 @@ namespace UnityShaderDecompiler.Tests; // Covers cbuffer emission tests. public class CbufferEmissionTests { - // Verifies that partial layout preserves buffer identity and raw byte offsets. + // Rejects overlapping raw storage instead of discarding the named member. [Theory] - [InlineData("MaterialParameters", "MaterialParameters")] - [InlineData("_Globals", "_GlobalsCB_b1")] - [Trait("Category", "D3D")] - public void PartialLayoutPreservesBufferIdentityAndRawByteOffsets(string name, string emittedName) + [InlineData("MaterialParameters")] + [InlineData("_Globals")] + public void PartialLayoutCannotReplaceKnownMembers(string name) { - if (!OperatingSystem.IsWindows()) - { - return; - } - HlslShaderNode shader = CreatePartialShader(name); - string text = HlslPrettyPrinter.Print(shader, fuseTemps: false); - string declaration = Regex.Match(text, @"cbuffer\b.*?};", RegexOptions.Singleline).Value; - Assert.NotEmpty(declaration); - string source = declaration + "\nfloat4 main() : SV_Target { return cb1_values[1]; }"; - DxbcFile compiled = new(); - compiled.Load(UnityShaderDecompiler.Core.DXBC.Compilation.DxbcCompiler.Compile(source, "main", "ps_5_0")); - - RdefConstantBuffer reflected = Assert.Single(compiled.ResourceDefinition!.ConstantBuffers); - Assert.Equal(emittedName, reflected.Name); - RdefVariable storage = Assert.Single(reflected.Variables); - Assert.Equal("cb1_values", storage.Name); - Assert.Equal(0u, storage.Offset); - Assert.Equal(32u, storage.Size); - Assert.Equal(32u, reflected.Size); - Assert.Equal(1u, compiled.ResourceDefinition.ResourceBindings.Single(b => b.Name == emittedName).BindPoint); + InvalidDataException error = Assert.Throws(() => + HlslPrettyPrinter.Print(shader, fuseTemps: false)); + Assert.Contains("Conflicting member layouts", error.Message); } // Builds a shader with overlapping named and raw constant-buffer entries. diff --git a/tests/UnityShaderDecompiler.Tests/ResourceBindingLayoutsTests.cs b/tests/UnityShaderDecompiler.Tests/ResourceBindingLayoutsTests.cs index 86d26d8..f362416 100644 --- a/tests/UnityShaderDecompiler.Tests/ResourceBindingLayoutsTests.cs +++ b/tests/UnityShaderDecompiler.Tests/ResourceBindingLayoutsTests.cs @@ -1,140 +1,107 @@ -using System; -using System.Linq; -using UnityShaderDecompiler.Core.Hlsl; +using UnityShaderDecompiler.Core.Hlsl; using UnityShaderDecompiler.Core.Hlsl.Ast; -using UnityShaderDecompiler.Core.Analysis; -using UnityShaderDecompiler.Core.Optimizations; -using UnityShaderDecompiler.Core.DXBC.Instructions; using UnityShaderDecompiler.Core.DXBC.IR; -using Xunit; namespace UnityShaderDecompiler.Tests; -// Covers resource binding layouts tests. +// Verifies that resource reconstruction never invents runtime bindings. public sealed class ResourceBindingLayoutsTests { - // Verifies that partial cbuffer metadata keeps raw register offsets. + // Keeps the original member name when the layout is fully identified. [Fact] - public void PartialCbufferMetadataKeepsRawRegisterOffsets() + public void KnownMembersRemainNamed() { - HlslShaderNode shader = CreatePartialShader(); - string source = HlslPrettyPrinter.Print(shader, fuseTemps: false); - Assert.Contains("cbuffer NamedBuffer : register(b1) { float4 cb1_values[2]; };", source); - Assert.Contains("cb1_values[0]", source); - Assert.DoesNotContain("NamedValue", source); + HlslResourceNode buffer = Buffer("NamedBuffer", "NamedValue"); + string source = HlslPrettyPrinter.Print(Shader(buffer), fuseTemps: false); + Assert.Contains("NamedValue : packoffset(c0)", source); + Assert.DoesNotContain("cb0_values", source); } - // Keeps declarations and reads distinct when two texture slots share a name. + // Refuses to erase known members when other accesses are unresolved. [Fact] - public void DuplicateTextureNamesUseDistinctIdentifiers() + public void PartialBufferCannotReplaceNamedMembersWithRawStorage() { - HlslShaderNode shader = new HlslShaderNode { Name = "Test/TextureSlots" }; - HlslSubShaderNode subShader = new HlslSubShaderNode(); - HlslPassNode pass = new HlslPassNode(); - pass.Resources.Add(new HlslResourceNode { Name = "_Tex", Kind = HlslResourceKind.Texture, Slot = 0 }); - pass.Resources.Add(new HlslResourceNode { Name = "_Tex", Kind = HlslResourceKind.Texture, Slot = 1 }); - IRRegister resource = new IRRegister - { - RegisterType = RegisterType.Resource, Index = 1, SymbolicName = "_Tex", - }; - HlslFunctionNode function = new HlslFunctionNode { Name = "frag", Stage = HlslShaderStage.Fragment }; - function.Statements.Statements.Add(new HlslAssignmentStatement + HlslResourceNode buffer = Buffer("NamedBuffer", "NamedValue"); + buffer.Variables.Add(new HlslCBufferVariable { - Destination = Temp(0, 15), - Expression = new IRExpression.TextureOperationExpression - { - Operation = IRExpression.TextureOperation.Load, - Resource = resource, - Coordinates = new IRExpression.ConstantExpression - { - Kind = IRExpression.ConstantExpression.ConstantKind.Int, - RawValues = new uint[] { 0, 0, 0 }, - }, - }, + Name = "cb0_values", TypeName = "float4", Offset = 0, Size = 32, ArraySize = 2, }); - pass.FragmentFunction = function; - subShader.Passes.Add(pass); - shader.SubShaders.Add(subShader); - string source = HlslPrettyPrinter.Print(shader, fuseTemps: false); - Assert.Contains("Texture2D _Tex : register(t0);", source); - Assert.Contains("Texture2D _Tex_t1 : register(t1);", source); - Assert.Contains("_Tex_t1.Load", source); + InvalidDataException error = Assert.Throws(() => HlslPrettyPrinter.Print(Shader(buffer))); + Assert.Contains("Conflicting member layouts", error.Message); + Assert.Equal("NamedValue", buffer.Variables[0].Name); } - // Creates a temporary register with the requested index and lane mask. - private static IRRegister Temp(uint index, byte mask = 1) + // Rejects different buffer identities sharing a slot across stages. + [Fact] + public void DifferentStageBuffersCannotMergeBySlot() { - IRRegister register = new IRRegister - { - RegisterType = RegisterType.Temp, - Index = index, - Type = IRValueType.Float, - Mask = mask, - ComponentMode = Operand.OperandComponentMode.Mask, - }; - for (int component = 0; component < 4; component++) - { - if ((mask & (1 << component)) != 0) - { - register.SsaVersion[component] = 1; - } - } - return register; + List resources = new List(); + VerifiedResourceBindings.Attach(resources, Buffer("PerObject", "ObjectValue"), "Vertex"); + InvalidDataException error = Assert.Throws(() => + VerifiedResourceBindings.Attach(resources, Buffer("PerCamera", "CameraValue"), "Fragment")); + Assert.Contains("Vertex PerObject, Fragment PerCamera", error.Message); + Assert.Single(resources); } - // Builds a shader that reads raw storage from a partially described buffer. - private static HlslShaderNode CreatePartialShader() + // Allows matching identities and layouts to be shared by both stages. + [Fact] + public void MatchingStageBuffersShareTheirVerifiedLayout() { - HlslShaderNode shader = new HlslShaderNode { Name = "Test/CbufferLayout" }; - HlslSubShaderNode subShader = new HlslSubShaderNode(); - HlslPassNode pass = new HlslPassNode(); - HlslResourceNode resource = CreatePartialBuffer(); - pass.Resources.Add(resource); - IRRegister read = new IRRegister - { - RegisterType = RegisterType.ConstantBuffer, - Type = IRValueType.Float, - Mask = 15, - ComponentMode = Operand.OperandComponentMode.Mask, - }; - read.Indices.Add(1); - read.Indices.Add(0); - HlslFunctionNode function = new HlslFunctionNode { Name = "frag", Stage = HlslShaderStage.Fragment }; - function.Statements.Statements.Add(new HlslAssignmentStatement - { - Destination = Temp(0, 15), - Expression = new IRExpression.RegisterExpression { Register = read }, - }); - pass.FragmentFunction = function; - subShader.Passes.Add(pass); - shader.SubShaders.Add(subShader); - return shader; + List resources = new List(); + VerifiedResourceBindings.Attach(resources, Buffer("Shared", "Value"), "Vertex"); + VerifiedResourceBindings.Attach(resources, Buffer("Shared", "Value"), "Fragment"); + Assert.Single(Assert.Single(resources).Variables); } - // Creates overlapping named and raw entries for a partial buffer. - private static HlslResourceNode CreatePartialBuffer() + // Rejects overlapping members instead of redirecting reads to another name. + [Fact] + public void DifferentMembersAtTheSameOffsetAreRejected() { - HlslResourceNode resource = new HlslResourceNode + List resources = new List(); + VerifiedResourceBindings.Attach(resources, Buffer("Shared", "Left"), "Vertex"); + Assert.Throws(() => + VerifiedResourceBindings.Attach(resources, Buffer("Shared", "Right"), "Fragment")); + } + + // Does not rename a duplicate texture into an unbound material property. + [Fact] + public void DuplicateTextureNamesAreRejectedWithoutMutation() + { + HlslResourceNode first = new HlslResourceNode { Name = "_Tex", Kind = HlslResourceKind.Texture, Slot = 0 }; + HlslResourceNode second = new HlslResourceNode { Name = "_Tex", Kind = HlslResourceKind.Texture, Slot = 1 }; + Assert.Throws(() => HlslPrettyPrinter.Print(Shader(first, second))); + Assert.Equal("_Tex", second.Name); + } + + // Requires texture metadata instead of inventing t0 or a property name. + [Fact] + public void MissingTextureNameStopsResourceConstruction() + { + List declarations = new List { - Name = "NamedBuffer", - Kind = HlslResourceKind.ConstantBuffer, - Slot = 1, + new IRDeclaration.IRResourceDeclaration { Slot = 0 }, }; - resource.Variables.Add(new HlslCBufferVariable - { - Name = "NamedValue", - TypeName = "float4", - Offset = 0, - Size = 16, - }); - resource.Variables.Add(new HlslCBufferVariable - { - Name = "cb1_values", - TypeName = "float4", - Offset = 0, - Size = 32, - ArraySize = 2, - }); - return resource; + InvalidDataException error = Assert.Throws(() => HlslAstBuilder.BuildResources(declarations).ToList()); + Assert.Contains("Missing texture binding name", error.Message); + } + + // Creates a fully specified buffer for binding checks. + private static HlslResourceNode Buffer(string name, string member) + { + HlslResourceNode buffer = new HlslResourceNode { Name = name, Kind = HlslResourceKind.ConstantBuffer, Slot = 0 }; + buffer.Variables.Add(new HlslCBufferVariable { Name = member, TypeName = "float4", Offset = 0, Size = 16 }); + return buffer; + } + + // Wraps resources in a pass that the shader printer will process. + private static HlslShaderNode Shader(params HlslResourceNode[] resources) + { + HlslShaderNode shader = new HlslShaderNode { Name = "Test/Bindings" }; + HlslSubShaderNode subshader = new HlslSubShaderNode(); + HlslPassNode pass = new HlslPassNode(); + pass.Resources.AddRange(resources); + subshader.Passes.Add(pass); + shader.SubShaders.Add(subshader); + return shader; } } From 0a4e04eb89c569aacb6ad068ff952cee69d3a124 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Thu, 5 Nov 2026 14:20:04 +0100 Subject: [PATCH 90/93] Read referenced code records and preserve variant resource bindings Keep parameter records out of code parsing and reject malformed referenced records in restored-variant mode. Remove property-order texture binding overrides. Three reader tests pass independently; all 16 focused tests pass with the other correction branches integrated. Native reflection checks do not establish Unity rendering equivalence. --- src/UnityShaderDecompiler.Core/Decompiler.cs | 12 +-- .../SerializedProgramReader.cs | 57 ++++++++++++ tests/UnityRuntime/README.md | 12 +++ .../RuntimeVariantBindingTests.cs | 90 +++++++++++++++++++ .../RuntimeVariantEmissionTests.cs | 2 +- .../SerializedProgramReaderTests.cs | 66 ++++++++++++++ 6 files changed, 229 insertions(+), 10 deletions(-) create mode 100644 src/UnityShaderDecompiler.Core/SerializedProgramReader.cs create mode 100644 tests/UnityShaderDecompiler.Tests/RuntimeVariantBindingTests.cs create mode 100644 tests/UnityShaderDecompiler.Tests/SerializedProgramReaderTests.cs diff --git a/src/UnityShaderDecompiler.Core/Decompiler.cs b/src/UnityShaderDecompiler.Core/Decompiler.cs index 1abedc6..08f82e9 100644 --- a/src/UnityShaderDecompiler.Core/Decompiler.cs +++ b/src/UnityShaderDecompiler.Core/Decompiler.cs @@ -138,7 +138,8 @@ public static DecompilationResult Decompile( log.WriteLine("Loading SubPrograms..."); log.WriteLine(); - program.Read(reader, 0); + SerializedProgramReader.Read(program, reader, project.Metadata, + options.RestoreKeywordVariants, log); if (options.OutRoot is { } createRoot) Directory.CreateDirectory(createRoot); @@ -657,14 +658,7 @@ private static void BuildRuntimeVariantPasses(HlslShaderNode shader, List byIndex = candidates.ToDictionary(c => c.Index); HashSet covered = new HashSet(); Dictionary textureNames = new Dictionary(); - uint textureSlot = 0; - foreach (HlslPropertyNode property in shader.Properties) - { - if (property.Kind == HlslPropertyKind.Texture) - { - textureNames[textureSlot++] = property.Name; - } - } + // Property declaration order is not a per-program texture binding table. foreach (HlslPassNode shell in shells) { if (shell.DirectXPrograms.Count == 0) diff --git a/src/UnityShaderDecompiler.Core/SerializedProgramReader.cs b/src/UnityShaderDecompiler.Core/SerializedProgramReader.cs new file mode 100644 index 0000000..3f6d3af --- /dev/null +++ b/src/UnityShaderDecompiler.Core/SerializedProgramReader.cs @@ -0,0 +1,57 @@ +using AssetStudio; +using UnityShaderDecompiler.Core.DXBC.Metadata; + +namespace UnityShaderDecompiler.Core; + +// Reads code records using serialized references instead of interpreting parameter records as code. +internal static class SerializedProgramReader +{ + // Keeps legacy inputs supported while requiring exact indices when references exist. + internal static void Read(ShaderProgram program, BinaryReader reader, ShaderMetadata metadata, + bool strict, TextWriter log) + { + int[] indices = metadata.SubShaders.SelectMany(sub => sub.Passes) + .SelectMany(pass => pass.DirectXPrograms).Select(reference => reference.BlobIndex) + .Distinct().Order().ToArray(); + if (indices.Length == 0) + { + program.Read(reader, 0); + return; + } + foreach (int index in indices) + { + try + { + ReadEntry(program, reader, index); + } + catch (Exception error) when (error is IOException or InvalidDataException or ArgumentException or OverflowException) + { + if (strict) + { + throw new InvalidDataException($"Failed to read serialized code record {index}.", error); + } + log.WriteLine($"Failed to read serialized code record {index}: {error.Message}"); + } + } + } + + // Bounds each code parser to its declared record and rejects unavailable segments. + private static void ReadEntry(ShaderProgram program, BinaryReader reader, int index) + { + if (index < 0 || index >= program.entries.Length) + { + throw new InvalidDataException($"Serialized code index {index} is outside the blob table."); + } + ShaderSubProgramEntry entry = program.entries[index]; + if (entry.Segment != 0 || entry.Offset < 0 || entry.Length <= 0 || + (long)entry.Offset + entry.Length > reader.BaseStream.Length) + { + throw new InvalidDataException($"Invalid or unavailable serialized code record {index}."); + } + reader.BaseStream.Position = entry.Offset; + byte[] bytes = reader.ReadBytes(entry.Length); + using MemoryStream stream = new MemoryStream(bytes, writable: false); + using BinaryReader bounded = new BinaryReader(stream); + program.m_SubPrograms[index] = new ShaderSubProgram(bounded); + } +} diff --git a/tests/UnityRuntime/README.md b/tests/UnityRuntime/README.md index 98007fd..68b88a2 100644 --- a/tests/UnityRuntime/README.md +++ b/tests/UnityRuntime/README.md @@ -28,3 +28,15 @@ and matching `.variants.json` under a project asset folder, copy Run the same CLI command with `-executeMethod ShaderVariantChecks.Run`. It checks the shader hash, pass count and nonempty compiled bytecode for both stages of every manifest entry. Results go to `shader-variant-results.json`. + +The native `RuntimeVariantBindingTests` check recompiles two keyword states +with different reflected textures at the same slot. It verifies texture and +sampler names/slots and the constant member offset/size. Property declaration +order must not override reflected bindings. This check needs the arithmetic +and locale corrections when testing the combined PRs; the isolated branch +still has those pre-existing compilation failures. Native reflection does not +validate Unity sampler-state selection or application rendering. + +Serialized code indices also restrict which blob records are parsed. Parameter +records are not shader programs. Invalid referenced records fail in strict mode; +unreferenced records are never classified by a guessed program header. diff --git a/tests/UnityShaderDecompiler.Tests/RuntimeVariantBindingTests.cs b/tests/UnityShaderDecompiler.Tests/RuntimeVariantBindingTests.cs new file mode 100644 index 0000000..6cc990e --- /dev/null +++ b/tests/UnityShaderDecompiler.Tests/RuntimeVariantBindingTests.cs @@ -0,0 +1,90 @@ +using System.Text.Json; +using AssetStudio; +using UnityShaderDecompiler.Core; +using UnityShaderDecompiler.Core.DXBC; +using UnityShaderDecompiler.Core.DXBC.Compilation; + +namespace UnityShaderDecompiler.Tests; + +// Checks real resource reflection after recompiling each restored keyword state. +public sealed class RuntimeVariantBindingTests +{ + // Preserves per-variant resource names despite an unrelated texture property. + [Fact] + [Trait("Category", "D3D")] + public void KeywordVariantsRetainTheirOwnResourceBindings() + { + if (!OperatingSystem.IsWindows()) + { + return; + } + using TempDirectory folder = new TempDirectory(); + File.WriteAllBytes(Path.Combine(folder.Path, "blob.bin"), RuntimeVariantEmissionTests.BuildBlob(Programs())); + File.WriteAllText(Path.Combine(folder.Path, "metadata.json"), Metadata()); + DecompilationResult result = Decompiler.DecompileFolder(folder.Path, new DecompileOptions + { + RestoreKeywordVariants = true, ReconstructSurfaceShaders = false, + FuseTemps = false, Verbose = false, OutRoot = folder.Path, KeepRegisterBindings = true, + }); + Assert.Single(result.Passes); + Assert.Equal(2, result.Passes[0].VariantPrograms.Count); + string path = Path.Combine(folder.Path, "Test_Bindings.shader"); + Dictionary expectedTextures = new Dictionary { [1] = "_TextureA", [2] = "_TextureB" }; + List cases = RuntimeVariantManifest.Read(path); + Assert.Equal(2, cases.Count); + foreach (RuntimeCompilationCase variant in cases) + { + VerifyReflection(variant.Hlsl, expectedTextures[variant.Fragment]); + } + } + + // Requires the recompiled code to expose the original texture and constant. + private static void VerifyReflection(string hlsl, string expectedTexture) + { + DxbcFile compiled = new DxbcFile(); + compiled.Load(DxbcCompiler.Compile(hlsl, "frag", "ps_4_0")); + Assert.NotNull(compiled.ResourceDefinition); + Assert.Contains(compiled.ResourceDefinition.ResourceBindings, binding => binding.Name == expectedTexture && binding.BindPoint == 0); + Assert.Contains(compiled.ResourceDefinition.ResourceBindings, binding => binding.Name == "sourceSampler" && binding.BindPoint == 0); + Assert.DoesNotContain(compiled.ResourceDefinition.ResourceBindings, binding => binding.Name == "_UnrelatedProperty"); + Assert.Contains(compiled.ResourceDefinition.ConstantBuffers.SelectMany(buffer => buffer.Variables), + variable => variable.Name == "_Tint" && variable.Offset == 0 && variable.Size == 16); + } + + // Builds independently reflected programs with different names on texture slot zero. + private static List<(ShaderGpuProgramType Stage, byte[] Code)> Programs() + { + List<(ShaderGpuProgramType Stage, byte[] Code)> programs = new List<(ShaderGpuProgramType, byte[])> + { + (ShaderGpuProgramType.DX11VertexSM40, DxbcCompiler.Compile("float4 main(float4 p:POSITION):SV_POSITION{return p;}", "main", "vs_4_0")), + }; + foreach (string name in new[] { "_TextureA", "_TextureB" }) + { + string source = $"Texture2D {name}:register(t0); SamplerState sourceSampler:register(s0); " + + "cbuffer Parameters:register(b0){float4 _Tint;} " + + $"float4 main():SV_Target{{return {name}.SampleLevel(sourceSampler,float2(0.5,0.5),0)*_Tint;}}"; + programs.Add((ShaderGpuProgramType.DX11PixelSM40, DxbcCompiler.Compile(source, "main", "ps_4_0"))); + } + return programs; + } + + // Describes a shared vertex and two fragment keyword states in one pass. + private static string Metadata() + { + return JsonSerializer.Serialize(new + { + name = "Test/Bindings", + properties = new[] { new { name = "_UnrelatedProperty", type = 4 } }, + keywordFlags = new { MODE = 0 }, + subShaders = new[] { new { passes = new[] { new + { + directXPrograms = new[] + { + new { blobIndex = 0, stage = "Vertex", keywords = Array.Empty(), keywordMask = Array.Empty() }, + new { blobIndex = 1, stage = "Fragment", keywords = Array.Empty(), keywordMask = new[] { "MODE" } }, + new { blobIndex = 2, stage = "Fragment", keywords = new[] { "MODE" }, keywordMask = new[] { "MODE" } }, + }, + } } } }, + }); + } +} diff --git a/tests/UnityShaderDecompiler.Tests/RuntimeVariantEmissionTests.cs b/tests/UnityShaderDecompiler.Tests/RuntimeVariantEmissionTests.cs index e85fd6b..447e652 100644 --- a/tests/UnityShaderDecompiler.Tests/RuntimeVariantEmissionTests.cs +++ b/tests/UnityShaderDecompiler.Tests/RuntimeVariantEmissionTests.cs @@ -122,7 +122,7 @@ public void NativeProgramsKeepTheirPassesAndSelectByKeywords() } // Serializes compiled fixture programs into a Unity shader blob. - private static byte[] BuildBlob(List<(ShaderGpuProgramType Stage, byte[] Code)> programs) + internal static byte[] BuildBlob(List<(ShaderGpuProgramType Stage, byte[] Code)> programs) { List payloads = new List(); foreach ((ShaderGpuProgramType stage, byte[] code) in programs) diff --git a/tests/UnityShaderDecompiler.Tests/SerializedProgramReaderTests.cs b/tests/UnityShaderDecompiler.Tests/SerializedProgramReaderTests.cs new file mode 100644 index 0000000..916434e --- /dev/null +++ b/tests/UnityShaderDecompiler.Tests/SerializedProgramReaderTests.cs @@ -0,0 +1,66 @@ +using System.Text.Json; +using AssetStudio; +using UnityShaderDecompiler.Core; +using UnityShaderDecompiler.Core.DXBC.Metadata; + +namespace UnityShaderDecompiler.Tests; + +// Separates serialized code references from unrelated parameter records. +public sealed class SerializedProgramReaderTests +{ + // Does not interpret an unreferenced, deliberately invalid record as shader code. + [Fact] + public void UnreferencedRecordsAreNotParsed() + { + using TempDirectory folder = new TempDirectory(); + ShaderMetadata metadata = Metadata(folder, 0); + byte[] blob = Blob(); + BitConverter.GetBytes(1).CopyTo(blob, 20); // Invalid length for the second record. + using BinaryReader reader = new BinaryReader(new MemoryStream(blob)); + ShaderProgram program = new ShaderProgram(reader, new[] { 2022, 3, 62 }); + SerializedProgramReader.Read(program, reader, metadata, true, TextWriter.Null); + Assert.NotNull(program.m_SubPrograms[0]); + Assert.Null(program.m_SubPrograms[1]); + } + + // Fails explicitly if the same invalid record is referenced as shader code. + [Theory] + [InlineData(1)] + [InlineData(2)] + public void InvalidReferencedRecordStopsReading(int index) + { + using TempDirectory folder = new TempDirectory(); + ShaderMetadata metadata = Metadata(folder, index); + byte[] blob = Blob(); + BitConverter.GetBytes(1).CopyTo(blob, 20); + using BinaryReader reader = new BinaryReader(new MemoryStream(blob)); + ShaderProgram program = new ShaderProgram(reader, new[] { 2022, 3, 62 }); + InvalidDataException error = Assert.Throws(() => + SerializedProgramReader.Read(program, reader, metadata, true, TextWriter.Null)); + Assert.Contains($"serialized code record {index}", error.Message); + } + + // Creates two bounded code records without requiring native compilation. + private static byte[] Blob() + { + return RuntimeVariantEmissionTests.BuildBlob(new List<(ShaderGpuProgramType, byte[])> + { + (ShaderGpuProgramType.DX11VertexSM40, new byte[] { 1 }), + (ShaderGpuProgramType.DX11PixelSM40, new byte[] { 2 }), + }); + } + + // Names the only code record that the caller is allowed to parse. + private static ShaderMetadata Metadata(TempDirectory folder, int index) + { + string path = Path.Combine(folder.Path, "metadata.json"); + File.WriteAllText(path, JsonSerializer.Serialize(new + { + name = "Test/CodeRecords", subShaders = new[] { new { passes = new[] { new + { + directXPrograms = new[] { new { blobIndex = index, stage = "Vertex", keywords = Array.Empty(), keywordMask = Array.Empty() } }, + } } } }, + })); + return ShaderMetadata.Load(path); + } +} From 08de9f1afc126e54544b2fb0693278ff86036057 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Thu, 5 Nov 2026 14:20:05 +0100 Subject: [PATCH 91/93] Require strict conversion and exact serialized variant coverage Check pass counts, manifest hashes and keyword-to-program mappings against original serialized references, then recompile every restored variant. Four oracle guards pass. All three real shader cases currently fail strict conversion on missing resource metadata; extraction-only success is no longer sufficient. Unity rendering validation remains blocked. --- tests/Fixtures/VolumetricLights/README.md | 54 ++++++++++++------ tests/e2e/shader_variant_contract.py | 68 +++++++++++++++++++++++ tests/e2e/test_shader_variant_contract.py | 58 +++++++++++++++++++ tests/e2e/test_volumetric_lights.py | 15 +++++ 4 files changed, 179 insertions(+), 16 deletions(-) create mode 100644 tests/e2e/shader_variant_contract.py create mode 100644 tests/e2e/test_shader_variant_contract.py diff --git a/tests/Fixtures/VolumetricLights/README.md b/tests/Fixtures/VolumetricLights/README.md index cadbf8c..8919d98 100644 --- a/tests/Fixtures/VolumetricLights/README.md +++ b/tests/Fixtures/VolumetricLights/README.md @@ -27,24 +27,46 @@ dotnet build src/UnityShaderDecompiler.Cli/UnityShaderDecompiler.Cli.csproj -c R python tests/e2e/test_volumetric_lights.py ``` -The tests read expected vertex/fragment code indices directly from the original JSON, require a collected function with the matching stage for every index, and verify each saved DXBC artifact. Parameter-record indices are not code references: interpreting them as program headers can print misleading GLES labels and `No DXBC:` diagnostics. Such diagnostics alone do not establish a lost DirectX program. A missing expected program always fails. +The tests require strict conversion with `--fail-on-error`, `--no-fuse-temps` +and `--restore-keyword-variants`. They verify: -Coverage: BilateralBlur **13** programs (5 vertex / 8 fragment), BlitAdd **2** (1 / 1), and VolumetricLight **184** (76 / 108): **199 unique programs across the three fixtures**. These checks establish extraction and per-program decompilation coverage, not successful recompilation of generated HLSL, complete runtime keyword selection, or GPU rendering equivalence. +- Every DirectX vertex/fragment code reference is collected and saved as DXBC. +- The generated shader preserves the original pass count and order. +- The hash-bound manifest contains exactly the keyword/program pairs derived + independently from the original JSON, without missing or duplicate states. +- Every manifest variant recompiles through the native D3D compiler. -## Dependency and before/after validation +Run the independent oracle guards with: -**Merge [PR #6](https://github.com/Abby1591/Unity_Shader_Decompiler/pull/6) before [PR #14](https://github.com/Abby1591/Unity_Shader_Decompiler/pull/14).** No other PR is required for these assertions. - -| Implementation tested with the same corpus/tests | Result | -| --- | --- | -| Upstream `417f26b` | Three shader tests fail at extraction (`m_CompressedBlob`); four integrity/guard tests pass | -| PR #6 alone, `9677441` | All seven tests pass; all 199 expected DirectX programs are collected | -| All twelve correction PRs integrated with upstream `417f26b` | All seven tests pass; all 199 expected DirectX programs are collected | - -To repeat the comparison without copying or editing fixtures, build the Release CLI in each target checkout, then run this test file with `USD_REPO_ROOT` set to that checkout. For example, from this PR's repository root: - -```bash -USD_REPO_ROOT=../decompiler-under-test python tests/e2e/test_volumetric_lights.py +```sh +python tests/e2e/test_shader_variant_contract.py ``` -Keep #14 in draft until #6 is integrated into its base. Passing this corpus does not independently prove the necessity or correctness of the other correction PRs. +Coverage remains 199 unique programs across the three fixtures. Parameter-record +indices are not code references. Successful extraction alone is insufficient: +a strict conversion error fails the shader test, even when all programs were found. + +## Dependencies and remaining validation + +These stronger checks require PR #6 (input schema), PR #9 (runtime variants and +manifest verification), PR #8 (strict resource validation), and the separate +code-generation corrections. Keep this PR in draft while its prerequisites +are missing. The previous seven passing tests checked extraction only; that +historical result does not apply to this stronger gate. + +The current parameter extractor does not recover all resource bindings from +serialized parameter records. Strict conversion must fail until that extraction +is implemented. Do not weaken these checks or repair the generated shaders. + +Set `USD_REPO_ROOT` to a source checkout under test and build its Release CLI +before running the corpus. Set `USD_CORPUS_OUTPUT` to a fresh directory outside +the repository to retain successful artifacts for Unity validation. Existing +output directories are rejected to avoid reusing stale shaders. + +After the offline gate passes, use the `tests/UnityRuntime` import/variant checks +from PR #9 in a disposable Built-in project with Unity CLI, Editor **6000.0.75f1**, +and Direct3D 11. Require an error-free import and compilation of every manifest +entry. Then compare original and regenerated shaders with identical scene, +material, camera, texture and keyword inputs. The offline checks do not establish +resource-binding or image equivalence in Unity. That rendering validation remains +blocked until reconstruction succeeds; no black-screen case is considered fixed. diff --git a/tests/e2e/shader_variant_contract.py b/tests/e2e/shader_variant_contract.py new file mode 100644 index 0000000..d1c55be --- /dev/null +++ b/tests/e2e/shader_variant_contract.py @@ -0,0 +1,68 @@ +# Checks restored variants against the unmodified serialized shader references. +import hashlib +import json +import re + + +# Reads only explicitly serialized D3D11 programs and their keyword states. +def stage_programs(program, types, names): + mask = frozenset(names[index] for index in program["m_SerializedKeywordStateMask"]) + groups = [program["m_SubPrograms"], *program["m_PlayerSubPrograms"]] + result = set() + for group in groups: + for entry in group: + if entry["m_GpuProgramType"] in types: + enabled = frozenset(names[index] for index in entry["m_KeywordIndices"]) + if not enabled <= mask: + raise AssertionError("Enabled keyword is absent from the serialized stage mask") + result.add((entry["m_BlobIndex"], enabled, mask)) + if not result: + raise AssertionError("Missing serialized D3D11 stage") + return result + + +# Enumerates compatible pairs and rejects ambiguous serialized keyword states. +def expected_states(shader_pass, names): + vertices = stage_programs(shader_pass["progVertex"], {15, 16}, names) + fragments = stage_programs(shader_pass["progFragment"], {17, 18}, names) + states = {} + used_vertices, used_fragments = set(), set() + for vertex, vertex_keys, vertex_mask in vertices: + for fragment, fragment_keys, fragment_mask in fragments: + shared = vertex_mask & fragment_mask + if vertex_keys & shared != fragment_keys & shared: + continue + keys = tuple(sorted(vertex_keys | fragment_keys)) + pair = (vertex, fragment) + if keys in states and states[keys] != pair: + raise AssertionError("Ambiguous serialized keyword state") + states[keys] = pair + used_vertices.add(vertex) + used_fragments.add(fragment) + if used_vertices != {entry[0] for entry in vertices} or used_fragments != {entry[0] for entry in fragments}: + raise AssertionError("Unpaired serialized stage") + return states + + +# Requires the same pass order and complete keyword-to-program mapping. +def verify_manifest(data, shader_path): + text = shader_path.read_text(encoding="utf-8-sig") + manifest = json.loads(shader_path.with_suffix(".variants.json").read_text()) + digest = hashlib.sha256(text.encode("utf-8")).hexdigest() + if manifest["schemaVersion"] != 1 or manifest["shaderSha256"].lower() != digest: + raise AssertionError("Variant manifest does not match shader") + parsed = data["m_ParsedForm"] + passes = [entry for sub in parsed["m_SubShaders"] for entry in sub["m_Passes"]] + if len(manifest["passes"]) != len(passes) or len(re.findall(r"\bHLSLPROGRAM\b", text)) != len(passes): + raise AssertionError("Original pass count was not preserved") + for index, (original, emitted) in enumerate(zip(passes, manifest["passes"])): + if emitted["passIndex"] != index: + raise AssertionError("Original pass order was not preserved") + actual = {} + for variant in emitted["variants"]: + keys = tuple(sorted(variant["keywords"])) + if keys in actual or len(set(keys)) != len(keys): + raise AssertionError("Duplicate emitted keyword state") + actual[keys] = (variant["vertex"], variant["fragment"]) + if actual != expected_states(original, parsed["m_KeywordNames"]): + raise AssertionError(f"Pass {index}: emitted keyword/program mapping differs from serialized data") diff --git a/tests/e2e/test_shader_variant_contract.py b/tests/e2e/test_shader_variant_contract.py new file mode 100644 index 0000000..71167dc --- /dev/null +++ b/tests/e2e/test_shader_variant_contract.py @@ -0,0 +1,58 @@ +# Exercises the source-to-manifest oracle without running a decompiler. +import hashlib +import json +import tempfile +import unittest +from pathlib import Path +from shader_variant_contract import expected_states, verify_manifest + + +# Supplies minimal explicit serialized references for contract checks. +class ShaderVariantContractTests(unittest.TestCase): + # Describes one shared vertex and two fragment keyword states. + def source(self): + vertex = {"m_SubPrograms": [{"m_BlobIndex": 1, "m_GpuProgramType": 15, "m_KeywordIndices": []}], + "m_PlayerSubPrograms": [], "m_SerializedKeywordStateMask": []} + fragment = {"m_SubPrograms": [ + {"m_BlobIndex": 2, "m_GpuProgramType": 17, "m_KeywordIndices": []}, + {"m_BlobIndex": 3, "m_GpuProgramType": 17, "m_KeywordIndices": [0]}], + "m_PlayerSubPrograms": [], "m_SerializedKeywordStateMask": [0]} + return {"m_ParsedForm": {"m_KeywordNames": ["MODE"], "m_SubShaders": [ + {"m_Passes": [{"progVertex": vertex, "progFragment": fragment}]}]}} + + # Writes a hash-bound manifest and exercises the independent oracle. + def verify(self, variants, pass_count=1): + text = "HLSLPROGRAM\nENDHLSL\n" * pass_count + with tempfile.TemporaryDirectory() as temporary: + shader = Path(temporary) / "test.shader" + shader.write_bytes(text.encode("utf-8")) + shader.with_suffix(".variants.json").write_text(json.dumps({ + "schemaVersion": 1, "shaderSha256": hashlib.sha256(text.encode()).hexdigest(), + "passes": [{"passIndex": 0, "variants": variants}]})) + verify_manifest(self.source(), shader) + + # Accepts exactly the serialized keyword/program mapping. + def test_matching_manifest(self): + self.verify([{"vertex": 1, "fragment": 2, "keywords": []}, + {"vertex": 1, "fragment": 3, "keywords": ["MODE"]}]) + + # Rejects a shader that silently drops a serialized keyword state. + def test_missing_variant(self): + with self.assertRaisesRegex(AssertionError, "mapping differs"): + self.verify([{"vertex": 1, "fragment": 2, "keywords": []}]) + + # Rejects expanded variant passes that would change runtime pass indices. + def test_expanded_passes(self): + with self.assertRaisesRegex(AssertionError, "pass count"): + self.verify([], pass_count=2) + + # Rejects ambiguity instead of choosing an arbitrary program. + def test_ambiguous_source(self): + shader_pass = self.source()["m_ParsedForm"]["m_SubShaders"][0]["m_Passes"][0] + shader_pass["progFragment"]["m_SubPrograms"][1]["m_KeywordIndices"] = [] + with self.assertRaisesRegex(AssertionError, "Ambiguous"): + expected_states(shader_pass, ["MODE"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/e2e/test_volumetric_lights.py b/tests/e2e/test_volumetric_lights.py index a2c6866..48e79f5 100644 --- a/tests/e2e/test_volumetric_lights.py +++ b/tests/e2e/test_volumetric_lights.py @@ -4,11 +4,13 @@ import json import os import re +import shutil import subprocess import sys import tempfile import unittest from pathlib import Path +from shader_variant_contract import verify_manifest CORPUS_ROOT = Path(__file__).resolve().parents[2] ROOT = Path(os.environ.get("USD_REPO_ROOT", CORPUS_ROOT)).resolve() @@ -70,6 +72,7 @@ def check_shader(self, name): "--flat", "--out-dir", str(folder / "input")]) log = self.run_checked(["dotnet", str(CLI), str(folder / "input"), "--no-fuse-temps", "--save-subprograms", + "--restore-keyword-variants", "--fail-on-error", "--out-root", str(folder / "output")]) verify_programs(expected, log) for index in expected: @@ -82,6 +85,18 @@ def check_shader(self, name): self.assertIn('Shader "' + data["m_ParsedForm"]["m_Name"] + '"', text) self.assertIn("#pragma vertex", text) self.assertIn("#pragma fragment", text) + verify_manifest(data, shaders[0]) + self.run_checked(["dotnet", str(CLI), "--recompile-verify", str(shaders[0])]) + self.preserve_artifacts(name, folder / "output") + + # Keeps successful output for the separate Unity import and rendering checks. + def preserve_artifacts(self, name, output): + destination = os.environ.get("USD_CORPUS_OUTPUT") + if destination: + target = Path(destination).resolve() / name + if target.exists(): + raise AssertionError(f"Output already exists; use a fresh validation directory: {target}") + shutil.copytree(output, target) # Runs a pipeline command and rejects unsuccessful completion. def run_checked(self, command): From e7443f63a84cc660a2b4d19c6a0b442b31f11770 Mon Sep 17 00:00:00 2001 From: End3rbyte Date: Thu, 5 Nov 2026 16:49:28 +0100 Subject: [PATCH 92/93] Extract serialized D3D11 program parameters --- Extract.py | 14 +- README.md | 9 +- libs/metadata.py | 26 ++- libs/parameter_records.py | 114 +++++++++++++ libs/program_parameters.py | 158 +++++++++++++++++ .../DXBC/Metadata/Shadermetadata.cs | 6 +- .../ParameterSlotsVerifier.csproj | 11 ++ tests/ParameterSlotsVerifier/Program.cs | 159 ++++++++++++++++++ tests/test_program_parameters.py | 95 +++++++++++ 9 files changed, 578 insertions(+), 14 deletions(-) create mode 100644 libs/parameter_records.py create mode 100644 libs/program_parameters.py create mode 100644 tests/ParameterSlotsVerifier/ParameterSlotsVerifier.csproj create mode 100644 tests/ParameterSlotsVerifier/Program.cs create mode 100644 tests/test_program_parameters.py diff --git a/Extract.py b/Extract.py index 8be10b6..164b238 100644 --- a/Extract.py +++ b/Extract.py @@ -14,6 +14,7 @@ """ import argparse +import json import sys from pathlib import Path @@ -47,6 +48,7 @@ def main(): # 0.1 — blob shader = ShaderBlob(str(json_path)) blob = shader.get_blob() + metadata = ShaderMetadata(str(json_path)).build(blob) out_dir = Path(args.out_dir) if not args.flat: @@ -59,9 +61,9 @@ def main(): print(f"Saved {blob_path} ({len(blob)} bytes)") # 0.2 — metadata - meta = ShaderMetadata(str(json_path)) meta_path = out_dir / "metadata.json" - meta.save(str(meta_path)) + with open(meta_path, "w", encoding="utf8") as stream: + json.dump(metadata, stream, indent=2) print(f"Saved {meta_path}") # 0.3 — dummy shader (best effort, not required) @@ -72,8 +74,12 @@ def main(): print(f"No dummy export found for '{shader.name}' in {args.dummy_dir}") print() - print(f"Ready for C# side:") - print(f" dotnet run --project \"src/UnityShaderDecompiler.Cli/UnityShaderDecompiler.Cli.csproj\" -- \"{blob_path}\"") + if any("programParameters" in shader_pass for subshader in metadata["subShaders"] + for shader_pass in subshader["passes"]): + print("Per-program parameters extracted; HLSL binding support is required before decompilation.") + else: + print("Ready for C# side:") + print(f" dotnet run --project \"src/UnityShaderDecompiler.Cli/UnityShaderDecompiler.Cli.csproj\" -- \"{blob_path}\"") if __name__ == "__main__": diff --git a/README.md b/README.md index 0ebc8db..8e7b722 100644 --- a/README.md +++ b/README.md @@ -112,9 +112,16 @@ the source JSON. Missing or invalid offsets and conflicting schema aliases fail | File | Contents | | --- | --- | | `blob.bin` | `m_CompressedBlob`, base64-decoded and LZ4-decompressed | -| `metadata.json` | flattened `m_ParsedForm`: properties, subshaders, render state, cbuffer layouts | +| `metadata.json` | flattened `m_ParsedForm` and, when present, exact per-program D3D11 parameter records | | `dummy.shader` | optional plain-text reference from `shaders_dummy/`, when a match exists | +For exports with `m_ParameterBlobIndices`, each pass has `programParameters` +entries keyed by stage, tier, program position, code index, and parameter index. +Binary and common parameters remain separate. The C# loader rejects these +entries until HLSL resource binding consumes them; it will not substitute the +legacy cross-pass constant-buffer layout. Check DXBC declaration coverage with +`dotnet run --project tests/ParameterSlotsVerifier -- Output/`. + Then build and run the decompiler: ```bash diff --git a/libs/metadata.py b/libs/metadata.py index 9fb4ce6..a6abd36 100644 --- a/libs/metadata.py +++ b/libs/metadata.py @@ -9,6 +9,7 @@ import json from .shader_json import load_shader_json, parameter_offset +from .program_parameters import extract_program_parameters class ShaderMetadata: @@ -16,14 +17,15 @@ def __init__(self, json_path): self.json_path = json_path self.data = load_shader_json(json_path) - def build(self): + def build(self, blob=None): d = self.data parsed = d.get("m_ParsedForm", d) # some exports nest under m_ParsedForm + program_parameters = extract_program_parameters(d, blob) if blob is not None else None return { "name": d.get("m_Name") or parsed.get("m_Name"), "properties": self._properties(parsed), - "subShaders": self._sub_shaders(parsed), + "subShaders": self._sub_shaders(parsed, program_parameters), "fallback": self._get(parsed, "m_FallbackName", ""), "customEditor": self._get(parsed, "m_CustomEditorName", ""), "keywords": self._get(parsed, "m_KeywordNames", []), @@ -66,8 +68,9 @@ def _properties(self, parsed): }) return out - def _sub_shaders(self, parsed): + def _sub_shaders(self, parsed, program_parameters=None): subshaders = [] + pass_index = 0 for ss in self._get(parsed, "m_SubShaders", []): raw_passes = ss.get("m_Passes", []) # Union cbuffer layouts by buffer name across every pass of @@ -77,11 +80,16 @@ def _sub_shaders(self, parsed): # them), but the layout of a same-named buffer is identical # wherever it appears, so a pass that omits one can still name # its reads from another pass's copy. - name_layouts = self._name_layouts(raw_passes) + name_layouts = self._name_layouts(raw_passes) if program_parameters is None else None + passes = [self._pass(p, name_layouts, program_parameters is None) for p in raw_passes] + if program_parameters is not None: + for shader_pass in passes: + shader_pass["programParameters"] = program_parameters[pass_index] + pass_index += 1 subshaders.append({ "tags": self._tags(ss.get("m_Tags", {})), "lod": ss.get("m_LOD"), - "passes": [self._pass(p, name_layouts) for p in raw_passes], + "passes": passes, }) return subshaders @@ -127,7 +135,7 @@ def _name_layouts(raw_passes): variables.append(entry) return layouts - def _pass(self, p, name_layouts=None): + def _pass(self, p, name_layouts=None, legacy_buffers=True): state = p.get("m_State", {}) prog_keys = ["m_ProgVertex", "m_ProgFragment", "m_ProgGeometry", "m_ProgHull", "m_ProgDomain", "m_ProgRayTracing"] @@ -148,17 +156,19 @@ def _pass(self, p, name_layouts=None): if subprograms: programs[key[2:]] = {"subProgramCount": len(subprograms)} - return { + result = { "name": p.get("m_Name"), "nameIndices": name_to_index, "tags": self._tags(state.get("m_Tags", {})), "renderState": self._render_state(state), "programs": programs, - "constantBuffers": self._constant_buffers(p, prog_keys, index_to_name, name_layouts), "textures": p.get("m_SerializedProgram", {}).get("m_TextureParameters", []), "samplers": p.get("m_SerializedProgram", {}).get("m_SamplerParameters", []), "keywords": p.get("m_NameIndices", []), } + if legacy_buffers: + result["constantBuffers"] = self._constant_buffers(p, prog_keys, index_to_name, name_layouts) + return result def _constant_buffers(self, p, prog_keys, index_to_name, name_layouts=None): # Unity serializes the constant-buffer layout (name + per-variable diff --git a/libs/parameter_records.py b/libs/parameter_records.py new file mode 100644 index 0000000..a0963c7 --- /dev/null +++ b/libs/parameter_records.py @@ -0,0 +1,114 @@ +# Reads versioned Unity shader parameter records without assigning bindings. +"""Parse the parameter records referenced by serialized D3D11 programs.""" + +import struct + + +# Reads bounds-checked fields from one parameter record. +class ParameterReader: + # Starts at the beginning of a record. + def __init__(self, data): + self.data = data + self.position = 0 + + # Reads one signed 32-bit field. + def integer(self): + if self.position + 4 > len(self.data): + raise ValueError("Truncated shader parameter record") + value = struct.unpack_from(" (len(self.data) - self.position) // 4: + raise ValueError(f"Invalid shader parameter count: {value}") + return value + + # Reads a padded UTF-8 identifier. + def string(self): + length = self.integer() + end = self.position + length + if length < 0 or end > len(self.data): + raise ValueError("Invalid shader parameter name length") + value = self.data[self.position:end].decode("utf-8") + self.position = (end + 3) & ~3 + if self.position > len(self.data): + raise ValueError("Truncated shader parameter name padding") + return value + + +# Reads a vector or matrix member with its serialized layout. +def read_variable(reader): + name = reader.string() + kind, rows, columns, matrix, array_size, offset = (reader.integer() for _ in range(6)) + if not name or kind not in range(6) or rows not in range(1, 5) or columns not in range(1, 5): + raise ValueError(f"Unsupported shader parameter shape: {name!r}") + if offset < 0 or offset % 4 or array_size < 0 or matrix not in (0, 1): + raise ValueError(f"Invalid shader parameter layout: {name}") + if not matrix and rows != 1: + raise ValueError(f"Unsupported nonmatrix parameter rows: {name}") + return {"name": name, "type": kind, "rows": rows, "columns": columns, + "isMatrix": bool(matrix), "arraySize": array_size, "offset": offset} + + +# Reads a serialized aggregate and all of its members. +def read_structure(reader): + name = reader.string() + offset, array_size, size = (reader.integer() for _ in range(3)) + if not name or offset < 0 or offset % 4 or array_size < 0 or size < 0: + raise ValueError(f"Invalid shader structure layout: {name!r}") + members = [read_variable(reader) for _ in range(reader.count())] + return {"name": name, "offset": offset, "arraySize": array_size, + "size": size, "members": members} + + +# Reads a named constant buffer and its complete member lists. +def read_buffer(reader): + name, size = reader.string(), reader.integer() + if size < 0: + raise ValueError(f"Negative constant-buffer size: {name}") + vectors = [read_variable(reader) for _ in range(reader.count())] + structures = [read_structure(reader) for _ in range(reader.count())] + if not name and (size or vectors or structures): + raise ValueError("Unbound global constants in shader parameter record") + members = vectors + structures + if any(item["offset"] >= size for item in members): + raise ValueError(f"Constant-buffer member exceeds {name} size") + if len({item["name"] for item in members}) != len(members): + raise ValueError(f"Duplicate constant-buffer member name: {name}") + return {"name": name, "size": size, "variables": vectors, "structures": structures} + + +# Reads one resource and preserves its native numeric fields. +def read_resource(reader): + name, kind = reader.string(), reader.integer() + if not name or kind not in range(5): + raise ValueError(f"Unsupported shader resource type: {name!r} ({kind})") + fields = [reader.integer() for _ in range(3 if kind == 0 else 2)] + if kind == 0: + if fields[0] < 0 or fields[1] < -1 or fields[2] < 0: + raise ValueError(f"Invalid texture resource fields: {name}") + return {"name": name, "kind": kind, "slot": fields[0], + "samplerIndex": fields[1], "dimensionFlags": fields[2]} + if fields[0] < 0: + raise ValueError(f"Negative shader resource slot: {name}") + return {"name": name, "kind": kind, "slot": fields[0], "extra": fields[1]} + + +# Parses one complete binary parameter record. +def read_parameters(data): + reader = ParameterReader(data) + version = reader.integer() + if version != 202012090: + raise ValueError(f"Unsupported shader parameter record version: {version}") + buffers = [read_buffer(reader) for _ in range(reader.count())] + resources = [read_resource(reader) for _ in range(reader.count())] + if reader.position != len(data): + raise ValueError(f"Unread shader parameter bytes: {len(data) - reader.position}") + if len({item['name'] for item in buffers}) != len(buffers): + raise ValueError("Duplicate constant-buffer name in parameter record") + if len({(item['kind'], item['slot']) for item in resources}) != len(resources): + raise ValueError("Duplicate resource slot in parameter record") + return {"version": version, "constantBuffers": buffers, "resources": resources} diff --git a/libs/program_parameters.py b/libs/program_parameters.py new file mode 100644 index 0000000..7ec9b9d --- /dev/null +++ b/libs/program_parameters.py @@ -0,0 +1,158 @@ +# Associates Unity's binary parameter records with exact serialized programs. +"""Extract per-pass, per-stage D3D11 parameter references from shader JSON.""" + +import copy + +from .parameter_records import ParameterReader, read_parameters + + +STAGES = {"Vertex": (15, 16), "Fragment": (17, 18), + "Geometry": (19, 20), "Hull": (21,), "Domain": (22,)} + + +# Checks one program-table entry against the decompressed shader blob. +def _record(blob, entries, index): + if type(index) is not int or not 0 <= index < len(entries): + raise ValueError(f"Invalid shader blob index: {index!r}") + offset, length, segment = entries[index] + if segment != 0 or offset < 4 + len(entries) * 12 or length < 8: + raise ValueError(f"Invalid shader blob entry: {index}") + if offset + length > len(blob): + raise ValueError(f"Shader blob entry exceeds its segment: {index}") + return blob[offset:offset + length] + + +# Reads the serialized program/parameter table for one D3D11 segment. +def _entries(shader, blob): + if shader.get("m_Platforms") != [4]: + raise ValueError("Per-program parameters require one D3D11 shader segment") + reader = ParameterReader(blob) + entries = [tuple(reader.integer() for _ in range(3)) for _ in range(reader.count())] + if not entries: + raise ValueError("Shader blob has no program entries") + return entries + + +# Resolves a pass-local name index without guessing missing names. +def _name(index, names): + if type(index) is not int or index not in names: + raise ValueError(f"Missing serialized shader name index: {index!r}") + return names[index] + + +# Adds a resolved name to a copied common-parameter object. +def _resolve(value, names): + if isinstance(value, list): + return [_resolve(item, names) for item in value] + if not isinstance(value, dict): + return value + result = {key: _resolve(item, names) for key, item in value.items()} + if "m_NameIndex" in value: + result["resolvedName"] = _name(value["m_NameIndex"], names) + return result + + +# Reads and validates the names serialized within one pass. +def _names(shader_pass): + mapping = shader_pass.get("m_NameIndices") + if not isinstance(mapping, dict): + raise ValueError("Missing serialized pass name indices") + result = {} + for name, index in mapping.items(): + if not isinstance(name, str) or not name or type(index) is not int or index < 0 or index in result: + raise ValueError(f"Invalid or duplicate serialized name index: {index!r}") + result[index] = name + return result + + +# Checks common and binary identities where both serialize the same resource. +def _verify_common(common, binary): + resources = binary["resources"] + for kind, field in ((0, "m_TextureParams"), (1, "m_ConstantBufferBindings")): + by_name = {item["name"]: item for item in resources if item["kind"] == kind} + by_slot = {item["slot"]: item for item in resources if item["kind"] == kind} + for item in common[field]: + name = item["resolvedName"] + if name in by_name and item["m_Index"] != by_name[name]["slot"]: + raise ValueError(f"Conflicting serialized {name} binding slot") + if item["m_Index"] in by_slot and by_slot[item["m_Index"]]["name"] != name: + raise ValueError(f"Conflicting serialized resource name at slot {item['m_Index']}") + buffers = {item["name"]: item for item in binary["constantBuffers"]} + for item in common["m_ConstantBuffers"]: + name = item["resolvedName"] + if name in buffers and item["m_Size"] != buffers[name]["size"]: + raise ValueError(f"Conflicting serialized {name} buffer size") + + +# Reads one referenced code record and its paired parameter record. +def _context(blob, entries, code, parameter_id, stage, group_index, program_index, + common, keyword_names, mask, cache): + code_id = code["m_BlobIndex"] + code_record = _record(blob, entries, code_id) + header = ParameterReader(code_record) + if header.integer() != 202012090 or header.integer() != code["m_GpuProgramType"]: + raise ValueError(f"Shader code record {code_id} does not match its stage") + if code["m_GpuProgramType"] not in STAGES[stage]: + raise ValueError(f"Shader code record {code_id} has the wrong stage") + if parameter_id not in cache: + cache[parameter_id] = read_parameters(_record(blob, entries, parameter_id)) + binary = cache[parameter_id] + _verify_common(common, binary) + indices = code["m_KeywordIndices"] + if any(type(index) is not int or not 0 <= index < len(keyword_names) for index in indices + mask): + raise ValueError(f"Invalid shader keyword index in code record {code_id}") + return {"stage": stage, "groupIndex": group_index, "programIndex": program_index, + "blobIndex": code_id, "parameterBlobIndex": parameter_id, + "keywords": [keyword_names[index] for index in indices], + "keywordMask": [keyword_names[index] for index in mask], + "commonParameters": common, "binaryParameters": copy.deepcopy(binary)} + + +# Associates every referenced D3D11 code program with its parameter index. +def _stage(blob, entries, program, stage, names, keyword_names, cache): + groups = program.get("m_PlayerSubPrograms", []) + parameters = program.get("m_ParameterBlobIndices") + if parameters is None or len(groups) != len(parameters): + raise ValueError(f"Missing or mismatched {stage} parameter indices") + mask = program["m_SerializedKeywordStateMask"] + common = _resolve(program["m_CommonParameters"], names) + if any(field not in common or not isinstance(common[field], list) + for field in ("m_TextureParams", "m_ConstantBufferBindings", "m_ConstantBuffers")): + raise ValueError(f"Missing serialized {stage} common resource parameters") + result = [] + for group_index, (group, parameter_group) in enumerate(zip(groups, parameters)): + if len(group) != len(parameter_group): + raise ValueError(f"Mismatched {stage} program and parameter counts") + for program_index, (code, parameter_id) in enumerate(zip(group, parameter_group)): + if code["m_GpuProgramType"] in STAGES[stage]: + result.append(_context(blob, entries, code, parameter_id, stage, group_index, + program_index, common, keyword_names, mask, cache)) + else: + raise ValueError(f"Unsupported D3D11 program type in {stage}: {code['m_GpuProgramType']}") + return result + + +# Extracts contexts without merging different passes, tiers, or variants. +def extract_program_parameters(shader, blob): + parsed = shader.get("m_ParsedForm", shader) + passes = [shader_pass for subshader in parsed["m_SubShaders"] + for shader_pass in subshader["m_Passes"]] + modern = any("m_ParameterBlobIndices" in shader_pass.get("m_Prog" + stage, {}) + for shader_pass in passes for stage in STAGES) + if not modern: + return None + entries = _entries(shader, blob) + cache = {} + result = [] + for shader_pass in passes: + names = _names(shader_pass) + contexts = [] + if shader_pass.get("m_ProgRayTracing", {}).get("m_PlayerSubPrograms"): + raise ValueError("Ray-tracing parameter references are unsupported") + for stage in STAGES: + program = shader_pass.get("m_Prog" + stage, {}) + if program.get("m_PlayerSubPrograms") or program.get("m_ParameterBlobIndices"): + contexts.extend(_stage(blob, entries, program, stage, names, + parsed["m_KeywordNames"], cache)) + result.append(contexts) + return result diff --git a/src/UnityShaderDecompiler.Core/DXBC/Metadata/Shadermetadata.cs b/src/UnityShaderDecompiler.Core/DXBC/Metadata/Shadermetadata.cs index 204a4bd..6431687 100644 --- a/src/UnityShaderDecompiler.Core/DXBC/Metadata/Shadermetadata.cs +++ b/src/UnityShaderDecompiler.Core/DXBC/Metadata/Shadermetadata.cs @@ -175,6 +175,10 @@ public sealed class PassMetadata public static PassMetadata From(JsonElement el) { + if (el.TryGetProperty("programParameters", out JsonElement _)) + { + throw new InvalidDataException("Per-program shader parameters require HLSL binding support before decompilation."); + } var result = new PassMetadata { Name = el.TryGetProperty("name", out var n) ? n.GetString() ?? "" : "", @@ -271,4 +275,4 @@ public sealed class CbufferVariableMetadata : 0, IsMatrix = el.TryGetProperty("isMatrix", out var m) && m.ValueKind == JsonValueKind.True, }; -} \ No newline at end of file +} diff --git a/tests/ParameterSlotsVerifier/ParameterSlotsVerifier.csproj b/tests/ParameterSlotsVerifier/ParameterSlotsVerifier.csproj new file mode 100644 index 0000000..0c780b3 --- /dev/null +++ b/tests/ParameterSlotsVerifier/ParameterSlotsVerifier.csproj @@ -0,0 +1,11 @@ + + + Exe + net10.0 + enable + enable + + + + + diff --git a/tests/ParameterSlotsVerifier/Program.cs b/tests/ParameterSlotsVerifier/Program.cs new file mode 100644 index 0000000..39fc35e --- /dev/null +++ b/tests/ParameterSlotsVerifier/Program.cs @@ -0,0 +1,159 @@ +using System.Text.Json; +using AssetStudio; +using UnityShaderDecompiler.Core.DXBC; +using UnityShaderDecompiler.Core.DXBC.Extraction; +using UnityShaderDecompiler.Core.DXBC.Instructions; + +// Checks recovered resource slots against the shipped D3D11 declarations. +internal static class Program +{ + // Verifies one extracted shader folder and reports its context count. + private static int Main(string[] args) + { + if (args.Length != 1) + { + Console.Error.WriteLine("Usage: ParameterSlotsVerifier "); + return 2; + } + try + { + string folder = args[0]; + using JsonDocument metadata = JsonDocument.Parse(File.ReadAllText(Path.Combine(folder, "metadata.json"))); + using MemoryStream stream = new MemoryStream(File.ReadAllBytes(Path.Combine(folder, "blob.bin"))); + using BinaryReader reader = new BinaryReader(stream); + ShaderProgram program = new ShaderProgram(reader, new[] { 2022, 3, 62 }); + program.Read(reader, 0); + int contexts = VerifyPasses(metadata.RootElement, program); + Console.WriteLine($"Verified {contexts} D3D11 program contexts."); + return 0; + } + catch (Exception error) + { + Console.Error.WriteLine(error); + return 1; + } + } + + // Traverses serialized passes without merging variant contexts. + private static int VerifyPasses(JsonElement metadata, ShaderProgram program) + { + int count = 0; + foreach (JsonElement subshader in metadata.GetProperty("subShaders").EnumerateArray()) + { + foreach (JsonElement pass in subshader.GetProperty("passes").EnumerateArray()) + { + foreach (JsonElement context in pass.GetProperty("programParameters").EnumerateArray()) + { + VerifyContext(context, program); + count++; + } + } + } + if (count == 0) + { + throw new InvalidDataException("No D3D11 program parameters found."); + } + return count; + } + + // Compares one program's declarations with its serialized resource slots. + private static void VerifyContext(JsonElement context, ShaderProgram program) + { + int index = context.GetProperty("blobIndex").GetInt32(); + ShaderSubProgram? subprogram = program.m_SubPrograms[index]; + if (subprogram is null) + { + throw new InvalidDataException($"Missing D3D11 program {index}."); + } + UnityShaderBlob blob = UnityShaderBlob.Parse(subprogram.m_ProgramCode); + DxbcFile file = new DxbcFile(); + file.Load(blob.Dxbc); + if (file.Shader is null) + { + throw new InvalidDataException($"Missing DXBC shader program {index}."); + } + HashSet textures = Slots(context, 0, "m_TextureParams"); + HashSet buffers = Slots(context, 1, "m_ConstantBufferBindings"); + HashSet samplers = Samplers(context); + foreach (Instruction instruction in file.Shader.Instructions) + { + switch (instruction.Opcode) + { + case Opcode.DclResource: + Require(textures, instruction.Operands[0].RegisterIndex, index, "texture"); + break; + case Opcode.DclSampler: + Require(samplers, instruction.Operands[0].RegisterIndex, index, "sampler"); + break; + case Opcode.DclConstantBuffer: + Require(buffers, instruction.Operands[0].RegisterIndex, index, "constant buffer"); + break; + case Opcode.DclResourceRaw: + case Opcode.DclResourceStructured: + throw new InvalidDataException($"Program {index} declares an unsupported buffer resource."); + } + } + } + + // Collects slots from binary records and matching common parameters. + private static HashSet Slots(JsonElement context, int kind, string commonField) + { + HashSet result = new HashSet(); + foreach (JsonElement resource in context.GetProperty("binaryParameters").GetProperty("resources").EnumerateArray()) + { + if (resource.GetProperty("kind").GetInt32() == kind) + { + result.Add(resource.GetProperty("slot").GetUInt32()); + } + } + foreach (JsonElement resource in context.GetProperty("commonParameters").GetProperty(commonField).EnumerateArray()) + { + result.Add(resource.GetProperty("m_Index").GetUInt32()); + } + return result; + } + + // Collects explicitly serialized texture-to-sampler associations. + private static HashSet Samplers(JsonElement context) + { + HashSet result = new HashSet(); + foreach (JsonElement resource in context.GetProperty("binaryParameters").GetProperty("resources").EnumerateArray()) + { + int kind = resource.GetProperty("kind").GetInt32(); + if (kind == 0) + { + int sampler = resource.GetProperty("samplerIndex").GetInt32(); + if (sampler >= 0) + { + result.Add((uint)sampler); + } + } + if (kind == 4) + { + result.Add(resource.GetProperty("slot").GetUInt32()); + } + } + foreach (JsonElement texture in context.GetProperty("commonParameters").GetProperty("m_TextureParams").EnumerateArray()) + { + int index = texture.GetProperty("m_SamplerIndex").GetInt32(); + if (index >= 0) + { + result.Add((uint)index); + } + else if (index != -1) + { + throw new InvalidDataException($"Invalid serialized sampler index: {index}."); + } + } + return result; + } + + // Rejects a declaration with no serialized slot source. + private static void Require(HashSet slots, uint slot, int index, string kind) + { + if (!slots.Contains(slot)) + { + throw new InvalidDataException($"Program {index} declares {kind} slot {slot} without parameter metadata."); + } + } +} diff --git a/tests/test_program_parameters.py b/tests/test_program_parameters.py new file mode 100644 index 0000000..e26feea --- /dev/null +++ b/tests/test_program_parameters.py @@ -0,0 +1,95 @@ +# Tests exact per-program shader parameter association and corruption failures. +import struct +import unittest + +from libs.program_parameters import extract_program_parameters + + +# Encodes a padded Unity parameter identifier. +def name(value): + data = value.encode("utf-8") + return struct.pack(" Date: Thu, 5 Nov 2026 16:51:47 +0100 Subject: [PATCH 93/93] Restore D3D11-only conversion option --- src/UnityShaderDecompiler.Cli/Program.cs | 1 + src/UnityShaderDecompiler.Core/Decompiler.cs | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/src/UnityShaderDecompiler.Cli/Program.cs b/src/UnityShaderDecompiler.Cli/Program.cs index 974c6ec..154428b 100644 --- a/src/UnityShaderDecompiler.Cli/Program.cs +++ b/src/UnityShaderDecompiler.Cli/Program.cs @@ -168,6 +168,7 @@ static void Main(string[] args) ReconstructSurfaceShaders = !args.Contains("--no-surface-shaders"), KeepPasses = args.Contains("--keep-passes"), KeepRegisterBindings = args.Contains("--keep-register-bindings"), + D3d11Only = args.Contains("--d3d11-only"), Verbose = true, }; diff --git a/src/UnityShaderDecompiler.Core/Decompiler.cs b/src/UnityShaderDecompiler.Core/Decompiler.cs index 7697685..604affb 100644 --- a/src/UnityShaderDecompiler.Core/Decompiler.cs +++ b/src/UnityShaderDecompiler.Core/Decompiler.cs @@ -21,6 +21,8 @@ namespace UnityShaderDecompiler.Core; /// public sealed class DecompileOptions { + /// Process only serialized Direct3D 11 subprograms. + public bool D3d11Only { get; init; } /// Abort on decompilation errors instead of logging and continuing. public bool FailOnError { get; init; } /// Keep serialized passes and select their programs through Unity keywords. @@ -179,6 +181,14 @@ public static DecompilationResult Decompile( continue; } + if (options.D3d11Only && + (sp.m_ProgramType < ShaderGpuProgramType.DX11VertexSM40 || + sp.m_ProgramType > ShaderGpuProgramType.DX11DomainSM50)) + { + log.WriteLine($"Skipped non-D3D11 subprogram {i}: {sp.m_ProgramType}"); + continue; + } + log.WriteLine($"Type : {sp.m_ProgramType}"); if (options.DumpKeywords)