sharpemu/tests/SharpEmu.ShaderCompiler.Tests/Gen5ScalarLaneTransferTests.cs

312 lines
11 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using SharpEmu.ShaderCompiler;
using SharpEmu.ShaderCompiler.Vulkan;
using SharpEmu.ShaderCompiler.Resources;
using SharpEmu.ShaderCompiler.Tests.Resources;
using System.Buffers.Binary;
using Xunit;
namespace SharpEmu.ShaderCompiler.Tests;
public sealed class Gen5ScalarLaneTransferTests
{
[Fact]
public void DecoderContinuesPastEndProgramForForwardBranchTarget()
{
const ulong shaderAddress = 0x1000;
var memory = new TestCpuMemory(shaderAddress, 0x100);
uint[] words =
[
0xBF880001, // s_cbranch_execz +1 -> pc 0x8
0xBF810000, // s_endpgm on the fallthrough path
0xBF800000, // s_nop 0 at the taken target
0xBF810000, // s_endpgm on the taken path
];
Span<byte> shader = stackalloc byte[words.Length * sizeof(uint)];
for (var index = 0; index < words.Length; index++)
{
BinaryPrimitives.WriteUInt32LittleEndian(
shader[(index * sizeof(uint))..],
words[index]);
}
Assert.True(memory.TryWrite(shaderAddress, shader));
var ctx = new CpuContext(memory, Generation.Gen5);
Assert.True(
Gen5ShaderTranslator.TryDecodeProgram(
ctx,
shaderAddress,
out var program,
out var decodeError),
decodeError);
Assert.Equal(
["SCbranchExecz", "SEndpgm", "SNop", "SEndpgm"],
program.Instructions.Select(static instruction => instruction.Opcode));
Assert.Equal(
[0u, 4u, 8u, 12u],
program.Instructions.Select(static instruction => instruction.Pc));
}
[Fact]
public void ScalarBlockerOpcodesDecodeAndCompile()
{
const ulong shaderAddress = 0x1000;
var memory = new TestCpuMemory(shaderAddress, 0x100);
uint[] words =
[
0xBF130200, // s_cmp_lg_u64 s[0:1], s[2:3]
0xBE861404, // s_ff1_i32_b64 s6, s[4:5]
0xBEEB106A, // s_bcnt1_i32_b64 s107, s[106:107]
0xBE890908, // s_wqm_b32 s9, s8
0xBF810000, // s_endpgm
];
Span<byte> shader = stackalloc byte[words.Length * sizeof(uint)];
for (var index = 0; index < words.Length; index++)
{
BinaryPrimitives.WriteUInt32LittleEndian(
shader[(index * sizeof(uint))..],
words[index]);
}
Assert.True(memory.TryWrite(shaderAddress, shader));
var ctx = new CpuContext(memory, Generation.Gen5);
Assert.True(
Gen5ShaderTranslator.TryDecodeProgram(
ctx,
shaderAddress,
out var program,
out var decodeError),
decodeError);
Assert.Collection(
program.Instructions,
instruction => Assert.Equal("SCmpLgU64", instruction.Opcode),
instruction => Assert.Equal("SFF1I32B64", instruction.Opcode),
instruction => Assert.Equal("SBcnt1I32B64", instruction.Opcode),
instruction => Assert.Equal("SWqmB32", instruction.Opcode),
instruction => Assert.Equal("SEndpgm", instruction.Opcode));
var request = ResourceTestProgram.Request(program);
Assert.True(
Gen5SpirvTranslator.TryCompileProgram(request, out var compiled, out var compileError),
compileError);
var opcodes = ReadSpirvOpcodes(compiled.Spirv);
Assert.Contains((ushort)SpirvOp.INotEqual, opcodes);
Assert.Contains((ushort)SpirvOp.Select, opcodes);
Assert.Contains((ushort)SpirvOp.IMul, opcodes);
Assert.Contains((ushort)SpirvOp.BitCount, opcodes);
}
[Fact]
public void ScalarBlockerOpcodesMatchRdna2Semantics()
{
List<Gen5ShaderInstruction> instructions =
[
ScalarInstruction(
0,
Gen5ShaderEncoding.Sopc,
"SCmpLgU64",
[Gen5Operand.Scalar(0), Gen5Operand.Scalar(2)]),
ScalarInstruction(
4,
Gen5ShaderEncoding.Sop1,
"SFF1I32B64",
[Gen5Operand.Scalar(4)],
Gen5Operand.Scalar(6)),
ScalarInstruction(
8,
Gen5ShaderEncoding.Sop2,
"SCselectB32",
[
new Gen5Operand(Gen5OperandKind.LiteralConstant, 0xAA),
new Gen5Operand(Gen5OperandKind.LiteralConstant, 0xBB),
],
Gen5Operand.Scalar(7)),
ScalarInstruction(
12,
Gen5ShaderEncoding.Sop1,
"SWqmB32",
[Gen5Operand.Scalar(8)],
Gen5Operand.Scalar(9)),
ScalarInstruction(
16,
Gen5ShaderEncoding.Sop2,
"SCselectB32",
[
new Gen5Operand(Gen5OperandKind.LiteralConstant, 1),
new Gen5Operand(Gen5OperandKind.LiteralConstant, 0),
],
Gen5Operand.Scalar(10)),
ScalarInstruction(
20,
Gen5ShaderEncoding.Sop1,
"SBcnt1I32B64",
[Gen5Operand.Scalar(11)],
Gen5Operand.Scalar(13)),
ScalarInstruction(
24,
Gen5ShaderEncoding.Sop2,
"SCselectB32",
[
new Gen5Operand(Gen5OperandKind.LiteralConstant, 1),
new Gen5Operand(Gen5OperandKind.LiteralConstant, 0),
],
Gen5Operand.Scalar(14)),
ResourceTestProgram.EndProgram(28),
];
uint[] userData = [0, 0, 1, 0, 0, 0x100, 0, 0, 0x10, 0, 0, 0xF0F0_F0F0, 0x8000_0000];
uint[] resultRegisters = [6, 7, 9, 10, 13, 14];
instructions.RemoveAt(instructions.Count - 1);
for (var index = 0; index < resultRegisters.Length; index++)
{
instructions.Add(ResourceTestProgram.ScalarLoad(
28 + (uint)index * 8, resultRegisters[index], destination: 100));
}
instructions.Add(ResourceTestProgram.EndProgram(28 + (uint)resultRegisters.Length * 8));
var inputBytes = (uint)userData.Length * 8;
var initialized = userData.Select((value, index) =>
ResourceTestProgram.MoveScalar((uint)index * 8, (uint)index, value)).ToList();
initialized.AddRange(instructions.Select(instruction => instruction with { Pc = instruction.Pc + inputBytes }));
var plan = ResourceTestProgram.Extract(new Gen5ShaderProgram(0, initialized), userDataCount: 0);
var evaluator = new RuntimeValueEvaluator(plan, ResourceTestProgram.Inputs([]));
var actual = new List<uint>();
foreach (var access in plan.Accesses)
{
Assert.True(evaluator.Evaluate(access!.Handle!.Operands[0], out var value),
$"Cannot resolve s{resultRegisters[actual.Count]}: {access.Handle.Operands[0].Kind}.");
actual.Add(value);
}
Assert.Equal([40u, 0xAAu, 0xF0u, 1u, 17u, 1u], actual);
}
[Fact]
public void RelativeVectorSourceTracksM0AndCompilesDynamicRead()
{
const ulong shaderAddress = 0x1000;
var memory = new TestCpuMemory(shaderAddress, 0x100);
uint[] words =
[
0x7E6E870C, // v_movrels_b32 v55, v12
0xBF810000, // s_endpgm
];
Span<byte> shader = stackalloc byte[words.Length * sizeof(uint)];
for (var index = 0; index < words.Length; index++)
{
BinaryPrimitives.WriteUInt32LittleEndian(
shader[(index * sizeof(uint))..],
words[index]);
}
Assert.True(memory.TryWrite(shaderAddress, shader));
var ctx = new CpuContext(memory, Generation.Gen5);
Assert.True(
Gen5ShaderTranslator.TryDecodeProgram(
ctx,
shaderAddress,
out var program,
out var decodeError),
decodeError);
var move = Assert.Single(
program.Instructions,
instruction => instruction.Opcode == "VMovrelsB32");
Assert.Equal(
[Gen5Operand.Vector(12), Gen5Operand.Scalar(124)],
move.Sources);
Assert.Equal([Gen5Operand.Vector(55)], move.Destinations);
var request = ResourceTestProgram.Request(program);
Assert.True(
Gen5SpirvTranslator.TryCompileProgram(request, out var compiled, out var compileError),
compileError);
var opcodes = ReadSpirvOpcodes(compiled.Spirv);
Assert.Contains((ushort)SpirvOp.IAdd, opcodes);
Assert.Contains((ushort)SpirvOp.ULessThan, opcodes);
Assert.Contains((ushort)SpirvOp.Select, opcodes);
}
private static Gen5ShaderInstruction ScalarInstruction(
uint pc,
Gen5ShaderEncoding encoding,
string opcode,
IReadOnlyList<Gen5Operand> sources,
Gen5Operand? destination = null) =>
new(
pc,
encoding,
opcode,
[0u],
sources,
destination.HasValue ? [destination.Value] : [],
null);
private static IReadOnlyList<ushort> ReadSpirvOpcodes(byte[] spirv)
{
var opcodes = new List<ushort>();
for (var offset = 5 * sizeof(uint); offset < spirv.Length;)
{
var instruction = BinaryPrimitives.ReadUInt32LittleEndian(
spirv.AsSpan(offset));
var wordCount = checked((int)(instruction >> 16));
Assert.True(wordCount > 0);
opcodes.Add((ushort)instruction);
offset += wordCount * sizeof(uint);
}
return opcodes;
}
private sealed class TestCpuMemory(ulong baseAddress, int size) : ICpuMemory
{
private readonly byte[] _storage = new byte[size];
public bool TryRead(ulong virtualAddress, Span<byte> destination)
{
if (!TryResolve(virtualAddress, destination.Length, out var offset))
{
return false;
}
_storage.AsSpan(offset, destination.Length).CopyTo(destination);
return true;
}
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
{
if (!TryResolve(virtualAddress, source.Length, out var offset))
{
return false;
}
source.CopyTo(_storage.AsSpan(offset, source.Length));
return true;
}
private bool TryResolve(
ulong virtualAddress,
int length,
out int offset)
{
offset = 0;
if (virtualAddress < baseAddress)
{
return false;
}
var relative = virtualAddress - baseAddress;
if (relative + (ulong)length > (ulong)_storage.Length)
{
return false;
}
offset = (int)relative;
return true;
}
}
}