diff --git a/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.cs b/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.cs index cb0678bc..2ccdea8b 100644 --- a/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.cs +++ b/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.cs @@ -392,6 +392,10 @@ public static partial class Gen5MslTranslator if (input.ComponentCount is >= 1 and <= 4) { _vertexInputsByPc.TryAdd(input.Pc, input); + foreach (var aliasPc in input.AliasPcs ?? []) + { + _vertexInputsByPc.TryAdd(aliasPc, input); + } } } } diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs index 6b7425a1..d7a12dae 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs @@ -1365,14 +1365,18 @@ public static partial class Gen5SpirvTranslator variable, SpirvDecoration.Location, input.Location); - _vertexInputsByPc.TryAdd( - input.Pc, - new SpirvVertexInput( - variable, - type, - componentType, - input.ComponentCount, - componentKind)); + var vertexInput = new SpirvVertexInput( + variable, + type, + componentType, + input.ComponentCount, + componentKind); + _vertexInputsByPc.TryAdd(input.Pc, vertexInput); + foreach (var aliasPc in input.AliasPcs ?? []) + { + _vertexInputsByPc.TryAdd(aliasPc, vertexInput); + } + _interfaces.Add(variable); } } diff --git a/src/SharpEmu.ShaderCompiler/Gen5ShaderIr.cs b/src/SharpEmu.ShaderCompiler/Gen5ShaderIr.cs index 758eaf88..ebe90131 100644 --- a/src/SharpEmu.ShaderCompiler/Gen5ShaderIr.cs +++ b/src/SharpEmu.ShaderCompiler/Gen5ShaderIr.cs @@ -302,6 +302,12 @@ public sealed record Gen5GlobalMemoryBinding( public bool WriteBackToGuest { get; set; } = true; } +// One attribute per distinct guest stream view. AliasPcs carries the other +// fetch instructions that read the same view: uber-shaders fetch a stream from +// every material branch, and the scalar evaluator visits one instruction on +// several CFG paths. Both must resolve to this binding's single location, +// because Metal caps a vertex function at 31 attributes and one location per +// fetch instruction overruns that on UE's larger vertex shaders. public sealed record Gen5VertexInputBinding( uint Pc, uint Location, @@ -314,7 +320,8 @@ public sealed record Gen5VertexInputBinding( byte[] Data, int DataLength, bool DataPooled, - bool PerInstance = false); + bool PerInstance = false, + IReadOnlyList? AliasPcs = null); public sealed record Gen5ShaderEvaluation( IReadOnlyList InitialScalarRegisters, diff --git a/src/SharpEmu.ShaderCompiler/Gen5ShaderScalarEvaluator.cs b/src/SharpEmu.ShaderCompiler/Gen5ShaderScalarEvaluator.cs index c5e413df..cd50d238 100644 --- a/src/SharpEmu.ShaderCompiler/Gen5ShaderScalarEvaluator.cs +++ b/src/SharpEmu.ShaderCompiler/Gen5ShaderScalarEvaluator.cs @@ -281,6 +281,13 @@ public static class Gen5ShaderScalarEvaluator var globalMemoryBindings = new List(); var globalMemoryByAddress = new Dictionary<(uint ScalarAddress, ulong BaseAddress), Gen5GlobalMemoryBinding>(); var vertexInputBindings = new List(); + // Absolute element address plus record layout identifies the guest + // stream view an attribute reads, so every fetch that resolves to it + // shares one location instead of claiming a new one. + var vertexInputByView = + new Dictionary<(ulong Address, uint Stride, uint DataFormat, + uint NumberFormat, uint ComponentCount), int>(); + var vertexInputAliasPcs = new List>(); // Shared, cached, read-only: computed once per decoded program. The // set already includes every instruction's destination registers, so // the per-load additions the loop used to make are redundant. @@ -653,6 +660,41 @@ public static class Gen5ShaderScalarEvaluator return false; } + var vertexInputView = ( + SaturatingAdd( + vertexInputBinding.BaseAddress, + vertexInputBinding.OffsetBytes), + vertexInputBinding.Stride, + vertexInputBinding.DataFormat, + vertexInputBinding.NumberFormat, + vertexInputBinding.ComponentCount); + if (vertexInputByView.TryGetValue( + vertexInputView, + out var existingVertexInput)) + { + var aliasPcs = vertexInputAliasPcs[existingVertexInput]; + if (!aliasPcs.Contains(instruction.Pc)) + { + aliasPcs.Add(instruction.Pc); + } + + // Descriptors for one view agree on size, but a path + // that resolved a larger reachable range still has to + // win so the capture covers every fetch. + var aliasedBinding = vertexInputBindings[existingVertexInput]; + if (aliasedBinding.DataLength < vertexInputBinding.DataLength) + { + vertexInputBindings[existingVertexInput] = aliasedBinding with + { + DataLength = vertexInputBinding.DataLength, + }; + } + + continue; + } + + vertexInputByView[vertexInputView] = vertexInputBindings.Count; + vertexInputAliasPcs.Add([]); vertexInputBindings.Add(vertexInputBinding); continue; } @@ -803,6 +845,18 @@ public static class Gen5ShaderScalarEvaluator if (vertexInputBindings.Count != 0) { + for (var index = 0; index < vertexInputBindings.Count; index++) + { + if (vertexInputAliasPcs[index].Count != 0) + { + vertexInputBindings[index] = vertexInputBindings[index] with + { + AliasPcs = vertexInputAliasPcs[index], + }; + } + } + + TraceVertexInputShape(vertexInputBindings); if (!TryCaptureVertexInputData( ctx, vertexInputBindings, @@ -951,6 +1005,43 @@ public static class Gen5ShaderScalarEvaluator return true; } + private static readonly bool _traceVertexInputShape = + string.Equals( + Environment.GetEnvironmentVariable("SHARPEMU_TRACE_VERTEX_SHAPE"), + "1", + StringComparison.Ordinal); + + private static readonly HashSet _tracedVertexInputShapes = []; + + private static void TraceVertexInputShape( + IReadOnlyList bindings) + { + if (!_traceVertexInputShape) + { + return; + } + + var distinctPcs = bindings.Select(static binding => binding.Pc).Distinct().Count(); + var identities = bindings + .Select(static binding => + $"{binding.BaseAddress:X}/{binding.Stride}/{binding.OffsetBytes}/" + + $"{binding.DataFormat}/{binding.NumberFormat}/{binding.ComponentCount}") + .ToArray(); + var shape = + $"count={bindings.Count} distinct_pc={distinctPcs} " + + $"distinct_view={identities.Distinct().Count()} " + + $"views={string.Join(',', identities)}"; + lock (_tracedVertexInputShapes) + { + if (!_tracedVertexInputShapes.Add(shape)) + { + return; + } + } + + Console.Error.WriteLine($"[VERTEX-SHAPE] {shape}"); + } + private static void TraceTitleVertexInputs(IReadOnlyList bindings) { if (!string.Equals( diff --git a/tests/SharpEmu.Libs.Tests/Agc/Gen5VertexInputSpirvTests.cs b/tests/SharpEmu.Libs.Tests/Agc/Gen5VertexInputSpirvTests.cs index e27c848c..bdf43a96 100644 --- a/tests/SharpEmu.Libs.Tests/Agc/Gen5VertexInputSpirvTests.cs +++ b/tests/SharpEmu.Libs.Tests/Agc/Gen5VertexInputSpirvTests.cs @@ -114,6 +114,96 @@ public sealed class Gen5VertexInputSpirvTests } } + [Fact] + public void AliasedFetchInstructionsShareOneAttributeLocation() + { + // Metal caps a vertex function at 31 attributes, so every fetch that + // reads one guest stream view must resolve to that view's single + // location instead of declaring its own. + var firstFetch = CreateVertexFetch(0); + var secondFetch = CreateVertexFetch(4); + var end = new Gen5ShaderInstruction( + 8, + Gen5ShaderEncoding.Sopp, + "SEndpgm", + [], + [], + [], + null); + var state = new Gen5ShaderState( + new Gen5ShaderProgram(0, [firstFetch, secondFetch, end]), + [], + null); + var registers = new uint[256]; + var data = new byte[16]; + var evaluation = new Gen5ShaderEvaluation( + registers, + registers, + [], + [], + VertexInputs: + [ + new Gen5VertexInputBinding( + 0, + 0, + 4, + 10, + 0, + 0x1000, + 4, + 0, + data, + data.Length, + DataPooled: false, + AliasPcs: [4u]), + ]); + + Assert.True( + Gen5SpirvTranslator.TryCompileVertexShader( + state, + evaluation, + out var shader, + out var error), + error); + + var module = ParseModule(shader.Spirv); + var locations = module + .Where(candidate => + candidate.Opcode == SpirvOp.Decorate && + candidate.Operands.Length >= 3 && + candidate.Operands[1] == (uint)SpirvDecoration.Location) + .ToArray(); + var inputVariable = Assert.Single(locations).Operands[0]; + + // Both fetches must read that variable; an unaliased second fetch would + // fall through to the generic buffer path and leave only one load. + Assert.Equal( + 2, + module.Count(candidate => + candidate.Opcode == SpirvOp.Load && + candidate.Operands.Length >= 3 && + candidate.Operands[2] == inputVariable)); + } + + private static Gen5ShaderInstruction CreateVertexFetch(uint pc) => + new( + pc, + Gen5ShaderEncoding.Mubuf, + "BufferLoadFormatXyzw", + [], + [], + [], + new Gen5BufferMemoryControl( + 4, + 5, + 0, + 0, + 0, + IndexEnabled: true, + OffsetEnabled: false, + Glc: false, + Slc: false)); + private static IReadOnlyList ParseModule(byte[] spirv) { var instructions = new List();