mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-31 23:19:44 +08:00
eb0653eded
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.
233 lines
7.2 KiB
C#
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);
|
|
}
|