From eb0653ededf2833656c035d1add3f5d6628c530f Mon Sep 17 00:00:00 2001 From: kuba Date: Fri, 31 Jul 2026 11:12:26 +0200 Subject: [PATCH] fix(gpu): one vertex attribute per guest stream view (#718) The scalar evaluator gave every buffer_load_format instruction its own attribute location. Two things multiply those: the CFG walk visits one instruction on several paths, and an uber vertex shader fetches the same stream from every material branch. UE's larger shaders reached 56 bindings from 8 distinct views, and one reached 701 from 5. Metal caps a vertex function at 31 attributes, so MoltenVK failed the MSL compile with "'attribute' attribute parameter is out of bounds" and the surrounding vkCreateGraphicsPipelines returned ErrorInitializationFailed. Every draw using those pipelines was dropped, which is why Silent Hill: The Short Message rendered a black scene. The vertex buffer count drove Metal's buffer indices out of range too, giving the companion "cannot reserve 'buffer' resource location at index 0" failures. Key attributes by the guest stream view they read - absolute element address, record stride and format - and alias every other fetch that resolves to the same view onto that binding, so both translators map those instruction PCs to one input variable. On PPSA10112 this takes the worst shader from 56 attributes to 8 and pipeline failures from 840 to 0. --- .../Gen5MslTranslator.cs | 4 + .../Gen5SpirvTranslator.cs | 20 ++-- src/SharpEmu.ShaderCompiler/Gen5ShaderIr.cs | 9 +- .../Gen5ShaderScalarEvaluator.cs | 91 +++++++++++++++++++ .../Agc/Gen5VertexInputSpirvTests.cs | 90 ++++++++++++++++++ 5 files changed, 205 insertions(+), 9 deletions(-) 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();