Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/ShaderRipper.Tests/BundleTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ private sealed class Builder
public uint Version = 8;
public bool PadTo16 = true;
public bool BlocksInfoAtEnd;
public bool CompressInfo;
public List<(string path, byte[] data)> Nodes = new();

public byte[] Build()
Expand Down
80 changes: 80 additions & 0 deletions src/ShaderRipper.Tests/SharedPassProgramTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using ShaderRipper.Data;
using ShaderRipper.Game;

namespace ShaderRipper.Tests;

/// <summary>
/// Two passes whose compiled programs are byte-identical share one blob slot in
/// the serialized asset. The pass map must therefore carry every owner per
/// slot: keying a slot by a single pass silently dropped the program from every
/// other pass that declared it.
/// </summary>
[TestFixture]
public class SharedPassProgramTests
{
private static Pass PassWithBlob(int blobIndex, int cbCount)
{
var sp = new SubProgram { BlobIndex = blobIndex };
for (int i = 0; i < cbCount; i++)
sp.ConstantBuffers.Add(new AssetConstantBuffer
{
Name = $"cb{i}",
Members = { new AssetCbMember { Name = $"m{i}" } },
});
return new Pass { SubPrograms = { sp } };
}

private static Shader ShaderWith(params Pass[] passes)
{
return new Shader
{
ParsedForm = new ParsedForm
{
Name = "Test/SharedPasses",
SubShaders = { new SubShader { Passes = new List<Pass>(passes) } },
},
};
}

[Test]
public void SharedBlobSlotListsEveryOwnerPass()
{
var passA = PassWithBlob(7, 2);
var passB = PassWithBlob(7, 2);
var sh = ShaderWith(passA, passB);

var (passOf, spOf) = GameExport.PassMaps(sh);

Assert.That(passOf[7], Is.EquivalentTo(new[] { passA, passB }));
// Equal reflection: the first pass seen keeps the slot.
Assert.That(spOf[7], Is.SameAs(passA.SubPrograms[0]));
}

[Test]
public void DistinctBlobSlotsKeepTheirOwnPasses()
{
var passA = PassWithBlob(1, 2);
var passB = PassWithBlob(2, 2);
var sh = ShaderWith(passA, passB);

var (passOf, _) = GameExport.PassMaps(sh);

Assert.That(passOf[1], Is.EquivalentTo(new[] { passA }));
Assert.That(passOf[2], Is.EquivalentTo(new[] { passB }));
}

[Test]
public void RicherReflectionWinsTheSharedSlot()
{
var thin = PassWithBlob(3, 1);
var rich = PassWithBlob(3, 4);
var sh = ShaderWith(thin, rich);

var (_, spOf) = GameExport.PassMaps(sh);

Assert.That(spOf[3], Is.SameAs(rich.SubPrograms[0]));
}
}
53 changes: 46 additions & 7 deletions src/ShaderRipper/Game/GameExport.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using ShaderRipper.Data;
using ShaderRipper.Emit;
using ShaderRipper.Resolve;
Expand Down Expand Up @@ -1058,6 +1059,40 @@ private static void ExportShader(Shader sh, string name, string rel, string dirN
// only thing the decoded slot tells us.
var (passOf, spOf) = PassMaps(sh);

// Cross-platform sampler names: the GL-family variants of the same
// shader keep the sampler names in their GLSL source
// (UNITY_LOCATION(n) uniform sampler2D <name>) where the DXBC payload
// strips them. GL locations and DXBC texture registers are assigned
// in the same property order, so location N names tN. A
// Windows-only build has no GL-family slot, and the DXBC route then
// falls back to reflection and (honestly noted) inference.
var glslFs = new Dictionary<int, string>();
var glslVs = new Dictionary<int, string>();
for (int gi = 0; gi < (sh.Platforms?.Length ?? 0); gi++)
{
var ginfo = Plugins.PlatformCatalog.Get((int)sh.Platforms[gi]);
if (ginfo == null || ginfo.Format != "glsl") continue;
byte[] gdecoded;
try { gdecoded = BlobTool.DecodeBlob(sh, gi); }
catch { continue; }
if (gdecoded == null || gdecoded.Length == 0) continue;
foreach (var pr in ProgramSource.Extract(gdecoded))
{
if (pr.Format != BlobFormat.Glsl || string.IsNullOrEmpty(pr.Text)) continue;
// Stage split: a vertex program's texture table is not the
// fragment program's, and both compile from the same source.
var target = pr.Text.Contains("SV_Target") ? glslFs
: pr.Text.Contains("gl_Position") ? glslVs : null;
if (target == null) continue;
foreach (Match m in Regex.Matches(pr.Text,
@"(?:UNITY_LOCATION\((\d+)\)|layout\(location\s*=\s*(\d+)\))\s*uniform\s+sampler2D\s+([A-Za-z_]\w*)"))
{
int loc = int.Parse(m.Groups[1].Success ? m.Groups[1].Value : m.Groups[2].Value);
if (!target.TryGetValue(loc, out var nm)) target[loc] = m.Groups[3].Value;
}
}
}

// Every platform the game shipped, one at a time: d3d11 drives the
// DXBC route, the GLES/GL platforms drive the GLSL route, everything
// else is noted and skipped (no translator for it in this project).
Expand All @@ -1079,7 +1114,7 @@ private static void ExportShader(Shader sh, string name, string rel, string dirN
if (decoded == null || decoded.Length == 0) { s.Skipped++; continue; }

if (info.Format == "dxbc") ExportDxbc(sh, name, info, decoded, dir, dirName,
passOf, spOf, kwIndex, props, perPass, programs, origAsm, s);
passOf, spOf, kwIndex, props, perPass, programs, origAsm, s, null, null, glslVs, glslFs);
else if (info.Format == "glsl") ExportGlsl(sh, name, info, decoded, dir, passOf, spOf, kwIndex, perPass, s, origAsm);
else if (info.Format == "d3d9") ExportD3d9(sh, name, info, decoded, dir, passOf, spOf, perPass, origAsm, s);
else ExportPlugin(sh, name, info, decoded, dir, passOf, spOf, kwIndex, props, perPass, origAsm, s);
Expand Down Expand Up @@ -1265,7 +1300,7 @@ internal static string TranslateOne(Shader sh, string name, Stats s)
if (pre != null && pre.Count == 0) { s.Skipped++; continue; }

if (info.Format == "dxbc") ExportDxbc(sh, name, info, decoded, null, null,
passOf, spOf, kwIndex, props, perPass, false, false, s, null, pre);
passOf, spOf, kwIndex, props, perPass, false, false, s, null, pre, null, null);
else if (info.Format == "glsl") ExportGlsl(sh, name, info, decoded, null, passOf, spOf, kwIndex, perPass, s, false);
else if (info.Format == "d3d9") ExportD3d9(sh, name, info, decoded, null, passOf, spOf, perPass, false, s);
else ExportPlugin(sh, name, info, decoded, null, passOf, spOf, kwIndex, props, perPass, false, s, null);
Expand Down Expand Up @@ -1363,7 +1398,7 @@ internal static void TranslateProgramsOf(Shader sh, Stats s,
/// mapping is therefore a list per slot: keeping only one owner silently
/// dropped the program from every other pass that declared it.
/// </summary>
private static (Dictionary<int, List<Pass>> passOf, Dictionary<int, SubProgram> spOf) PassMaps(Shader sh, int platformIndex = -1)
internal static (Dictionary<int, List<Pass>> passOf, Dictionary<int, SubProgram> spOf) PassMaps(Shader sh, int platformIndex = -1)
{
var passOf = new Dictionary<int, List<Pass>>();
var spOf = new Dictionary<int, SubProgram>();
Expand Down Expand Up @@ -1456,7 +1491,8 @@ private static void ExportDxbc(Shader sh, string name, PlatformInfo info, byte[]
Dictionary<int, string> kwIndex, List<Property> props,
Dictionary<Pass, PassBuild> perPass, bool programs, bool origAsm, Stats s,
List<(int blobIndex, string stage, string hlsl)> collected = null,
List<BlobTool.BlobSlot> preSlots = null)
List<BlobTool.BlobSlot> preSlots = null,
Dictionary<int, string> glslVs = null, Dictionary<int, string> glslFs = null)
{
int platformIndex = Array.IndexOf(sh.Platforms, (uint)info.Id);
if (sh.ParsedForm?.SubShaders.SelectMany(ss => ss.Passes).SelectMany(p => p.SubPrograms)
Expand Down Expand Up @@ -1491,8 +1527,10 @@ private static void ExportDxbc(Shader sh, string name, PlatformInfo info, byte[]
List<Pass>? owners = null;
passOf.TryGetValue(slot.BlobIndex, out owners);
var pass = owners is { Count: > 0 } ? owners[0] : null;
var glslTexNames = module.Stage is "vs" ? glslVs
: module.Stage is "ps" ? glslFs : null;
module.Cb = NameConstantBuffers(module, name, pass, sub, props, paramCbs, allParamCbs,
matrixRows);
matrixRows, glslTexNames);

// Two consumers, two shapes. The ShaderLab wrapper merges this text
// into a file that includes `UnityCG.cginc` -- matrices are
Expand Down Expand Up @@ -2352,7 +2390,8 @@ private static Resolve.CbResolver NameConstantBuffers(
Ast.Module m, string shaderName, Pass pass, SubProgram sub,
List<Property> props, Dictionary<int, List<BlobTool.CbInfo>> paramCbs,
List<BlobTool.CbInfo> allParamCbs,
Dictionary<string, int> matrixRows = null)
Dictionary<string, int> matrixRows = null,
Dictionary<int, string> glslTexNames = null)
{
var nameTable = pass?.NameIndices != null
? new HashSet<string>(pass.NameIndices.Keys, StringComparer.Ordinal)
Expand All @@ -2366,7 +2405,7 @@ private static Resolve.CbResolver NameConstantBuffers(

var r = Resolve.CbResolver.Resolve(
m, nameTable, props, own, -1, BestGlobals(sub), null,
allParamCbs, true, null, sub?.TextureParams, matrixRows);
allParamCbs, true, null, sub?.TextureParams, matrixRows, glslTexNames);

if (Environment.GetEnvironmentVariable("SHADERRIP_CBDBG") == "1")
Console.Error.WriteLine($"[cb] {shaderName}/{sub?.BlobIndex}: "
Expand Down
Loading
Loading