Files
sharpemu/tests/SharpEmu.Libs.Tests/Agc/Gen5VertexInputSpirvTests.cs
T
kuba eb0653eded 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.
2026-07-31 12:12:26 +03:00

233 lines
7.2 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.ShaderCompiler;
using SharpEmu.ShaderCompiler.Vulkan;
using Xunit;
namespace SharpEmu.Libs.Tests.Agc;
public sealed class Gen5VertexInputSpirvTests
{
[Theory]
[InlineData(0u, null)]
[InlineData(4u, 0u)]
[InlineData(5u, 1u)]
public void VertexInputTypeMatchesGuestNumberFormat(
uint numberFormat,
uint? expectedIntegerSignedness)
{
var instruction = new Gen5ShaderInstruction(
0,
Gen5ShaderEncoding.Mubuf,
"BufferLoadFormatXyzw",
[],
[],
[],
new Gen5BufferMemoryControl(
4,
5,
0,
0,
0,
IndexEnabled: true,
OffsetEnabled: false,
Glc: false,
Slc: false));
var end = new Gen5ShaderInstruction(
4,
Gen5ShaderEncoding.Sopp,
"SEndpgm",
[],
[],
[],
null);
var state = new Gen5ShaderState(
new Gen5ShaderProgram(0, [instruction, 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,
numberFormat,
0x1000,
4,
0,
data,
data.Length,
DataPooled: false),
]);
Assert.True(
Gen5SpirvTranslator.TryCompileVertexShader(
state,
evaluation,
out var shader,
out var error),
error);
var module = ParseModule(shader.Spirv);
var inputVariable = module.Single(candidate =>
candidate.Opcode == SpirvOp.Decorate &&
candidate.Operands.Length >= 3 &&
candidate.Operands[1] == (uint)SpirvDecoration.Location &&
candidate.Operands[2] == 0).Operands[0];
var pointerType = module.Single(candidate =>
candidate.Opcode == SpirvOp.Variable &&
candidate.Operands[1] == inputVariable).Operands[0];
var vectorType = module.Single(candidate =>
candidate.Opcode == SpirvOp.TypePointer &&
candidate.Operands[0] == pointerType).Operands[2];
var componentType = module.Single(candidate =>
candidate.Opcode == SpirvOp.TypeVector &&
candidate.Operands[0] == vectorType).Operands[1];
if (expectedIntegerSignedness is { } signedness)
{
Assert.Contains(
module,
candidate =>
candidate.Opcode == SpirvOp.TypeInt &&
candidate.Operands[0] == componentType &&
candidate.Operands[1] == 32 &&
candidate.Operands[2] == signedness);
}
else
{
Assert.Contains(
module,
candidate =>
candidate.Opcode == SpirvOp.TypeFloat &&
candidate.Operands[0] == componentType &&
candidate.Operands[1] == 32);
}
}
[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)
{
var instructions = new List<ParsedInstruction>();
for (var offset = 5; offset < spirv.Length / sizeof(uint);)
{
var header = BitConverter.ToUInt32(spirv, offset * sizeof(uint));
var wordCount = (int)(header >> 16);
Assert.True(wordCount > 0);
var operands = new uint[wordCount - 1];
for (var index = 0; index < operands.Length; index++)
{
operands[index] = BitConverter.ToUInt32(
spirv,
(offset + index + 1) * sizeof(uint));
}
instructions.Add(
new ParsedInstruction((SpirvOp)(ushort)header, operands));
offset += wordCount;
}
return instructions;
}
private sealed record ParsedInstruction(SpirvOp Opcode, uint[] Operands);
}