mirror of
https://github.com/par274/sharpemu.git
synced 2026-08-01 15:39:47 +08:00
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.
This commit is contained in:
@@ -392,6 +392,10 @@ public static partial class Gen5MslTranslator
|
|||||||
if (input.ComponentCount is >= 1 and <= 4)
|
if (input.ComponentCount is >= 1 and <= 4)
|
||||||
{
|
{
|
||||||
_vertexInputsByPc.TryAdd(input.Pc, input);
|
_vertexInputsByPc.TryAdd(input.Pc, input);
|
||||||
|
foreach (var aliasPc in input.AliasPcs ?? [])
|
||||||
|
{
|
||||||
|
_vertexInputsByPc.TryAdd(aliasPc, input);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1365,14 +1365,18 @@ public static partial class Gen5SpirvTranslator
|
|||||||
variable,
|
variable,
|
||||||
SpirvDecoration.Location,
|
SpirvDecoration.Location,
|
||||||
input.Location);
|
input.Location);
|
||||||
_vertexInputsByPc.TryAdd(
|
var vertexInput = new SpirvVertexInput(
|
||||||
input.Pc,
|
variable,
|
||||||
new SpirvVertexInput(
|
type,
|
||||||
variable,
|
componentType,
|
||||||
type,
|
input.ComponentCount,
|
||||||
componentType,
|
componentKind);
|
||||||
input.ComponentCount,
|
_vertexInputsByPc.TryAdd(input.Pc, vertexInput);
|
||||||
componentKind));
|
foreach (var aliasPc in input.AliasPcs ?? [])
|
||||||
|
{
|
||||||
|
_vertexInputsByPc.TryAdd(aliasPc, vertexInput);
|
||||||
|
}
|
||||||
|
|
||||||
_interfaces.Add(variable);
|
_interfaces.Add(variable);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -302,6 +302,12 @@ public sealed record Gen5GlobalMemoryBinding(
|
|||||||
public bool WriteBackToGuest { get; set; } = true;
|
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(
|
public sealed record Gen5VertexInputBinding(
|
||||||
uint Pc,
|
uint Pc,
|
||||||
uint Location,
|
uint Location,
|
||||||
@@ -314,7 +320,8 @@ public sealed record Gen5VertexInputBinding(
|
|||||||
byte[] Data,
|
byte[] Data,
|
||||||
int DataLength,
|
int DataLength,
|
||||||
bool DataPooled,
|
bool DataPooled,
|
||||||
bool PerInstance = false);
|
bool PerInstance = false,
|
||||||
|
IReadOnlyList<uint>? AliasPcs = null);
|
||||||
|
|
||||||
public sealed record Gen5ShaderEvaluation(
|
public sealed record Gen5ShaderEvaluation(
|
||||||
IReadOnlyList<uint> InitialScalarRegisters,
|
IReadOnlyList<uint> InitialScalarRegisters,
|
||||||
|
|||||||
@@ -281,6 +281,13 @@ public static class Gen5ShaderScalarEvaluator
|
|||||||
var globalMemoryBindings = new List<Gen5GlobalMemoryBinding>();
|
var globalMemoryBindings = new List<Gen5GlobalMemoryBinding>();
|
||||||
var globalMemoryByAddress = new Dictionary<(uint ScalarAddress, ulong BaseAddress), Gen5GlobalMemoryBinding>();
|
var globalMemoryByAddress = new Dictionary<(uint ScalarAddress, ulong BaseAddress), Gen5GlobalMemoryBinding>();
|
||||||
var vertexInputBindings = new List<Gen5VertexInputBinding>();
|
var vertexInputBindings = new List<Gen5VertexInputBinding>();
|
||||||
|
// 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<List<uint>>();
|
||||||
// Shared, cached, read-only: computed once per decoded program. The
|
// Shared, cached, read-only: computed once per decoded program. The
|
||||||
// set already includes every instruction's destination registers, so
|
// set already includes every instruction's destination registers, so
|
||||||
// the per-load additions the loop used to make are redundant.
|
// the per-load additions the loop used to make are redundant.
|
||||||
@@ -653,6 +660,41 @@ public static class Gen5ShaderScalarEvaluator
|
|||||||
return false;
|
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);
|
vertexInputBindings.Add(vertexInputBinding);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -803,6 +845,18 @@ public static class Gen5ShaderScalarEvaluator
|
|||||||
|
|
||||||
if (vertexInputBindings.Count != 0)
|
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(
|
if (!TryCaptureVertexInputData(
|
||||||
ctx,
|
ctx,
|
||||||
vertexInputBindings,
|
vertexInputBindings,
|
||||||
@@ -951,6 +1005,43 @@ public static class Gen5ShaderScalarEvaluator
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static readonly bool _traceVertexInputShape =
|
||||||
|
string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_VERTEX_SHAPE"),
|
||||||
|
"1",
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
|
||||||
|
private static readonly HashSet<string> _tracedVertexInputShapes = [];
|
||||||
|
|
||||||
|
private static void TraceVertexInputShape(
|
||||||
|
IReadOnlyList<Gen5VertexInputBinding> 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<Gen5VertexInputBinding> bindings)
|
private static void TraceTitleVertexInputs(IReadOnlyList<Gen5VertexInputBinding> bindings)
|
||||||
{
|
{
|
||||||
if (!string.Equals(
|
if (!string.Equals(
|
||||||
|
|||||||
@@ -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<ParsedInstruction> ParseModule(byte[] spirv)
|
private static IReadOnlyList<ParsedInstruction> ParseModule(byte[] spirv)
|
||||||
{
|
{
|
||||||
var instructions = new List<ParsedInstruction>();
|
var instructions = new List<ParsedInstruction>();
|
||||||
|
|||||||
Reference in New Issue
Block a user