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

348 lines
12 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using SharpEmu.HLE;
using SharpEmu.ShaderCompiler;
using SharpEmu.ShaderCompiler.Resources;
using SharpEmu.ShaderCompiler.Tests.Resources;
using SharpEmu.ShaderCompiler.Vulkan;
using Xunit;
namespace SharpEmu.ShaderCompiler.Tests;
public sealed class Gen5FlatMemoryTests
{
private const ulong ShaderAddress = 0x1_0000_0000;
private const uint SEndpgm = 0xBF810000;
public static IEnumerable<object[]> GlobalMemoryRegisterCases()
{
foreach (var usesFlatAddress in new[] { false, true })
foreach (var opcode in new uint[] { 8, 9, 10, 11, 12, 13, 14, 15, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 50, 56 })
{
yield return [usesFlatAddress, opcode, false];
if (opcode is 50 or 56) yield return [usesFlatAddress, opcode, true];
}
}
[Theory]
[MemberData(nameof(GlobalMemoryRegisterCases))]
public void GlobalMemoryInstructions_KeepSeparateReadAndWriteRegisters(bool usesFlatAddress, uint opcode, bool returnsValue)
{
var word = 0xDC00_0000u | (opcode << 18) | (usesFlatAddress ? 0 : 2u << 14) | (returnsValue ? 1u << 16 : 0);
var instruction = DecodeProgram(word, 0x0810_0309, SEndpgm).Instructions[0];
var control = Assert.IsType<Gen5GlobalMemoryControl>(instruction.Control);
Assert.Equal(3u, control.SourceVectorRegister);
Assert.Equal(8u, control.DestinationVectorRegister);
Assert.Equal(9u, control.VectorAddress);
Assert.Equal(usesFlatAddress ? uint.MaxValue : 16u, control.ScalarAddress);
Assert.Equal(usesFlatAddress, control.UsesFlatAddress);
Assert.Equal(returnsValue, control.Glc);
var isLoad = opcode is >= 8 and <= 15 or >= 32 and <= 37;
var componentCount = opcode switch { 13 or 29 => 2, 14 or 30 => 4, 15 or 31 => 3, _ => 1 };
Assert.Equal((uint)componentCount, control.DwordCount);
Gen5Operand[] addressSources = usesFlatAddress
? [Gen5Operand.Vector(9), Gen5Operand.Vector(10)]
: [Gen5Operand.Vector(9), Gen5Operand.Scalar(16)];
var expectedSources = addressSources.ToList();
if (!isLoad)
expectedSources.AddRange(Enumerable.Range(3, componentCount).Select(index => Gen5Operand.Vector((uint)index)));
else if (opcode is >= 32 and <= 37)
expectedSources.Add(Gen5Operand.Vector(8));
Assert.Equal(expectedSources, instruction.Sources);
Assert.Equal(isLoad || returnsValue
? Enumerable.Range(8, componentCount).Select(index => Gen5Operand.Vector((uint)index))
: [], instruction.Destinations);
}
[Fact]
public void ReturningAtomic_InvalidatesThePreviousFlatAddressDefinition()
{
var program = DecodeProgram(
0x7E02_020C, 0x7E04_020D,
0xDCC9_8000, 0x0110_0309,
0xDC30_0000, 0x087D_0001,
SEndpgm);
var atomic = program.Instructions[2];
Assert.Equal("GlobalAtomicAdd", atomic.Opcode);
Assert.Equal([Gen5Operand.Vector(1)], atomic.Destinations);
var load = Assert.IsType<Gen5GlobalMemoryControl>(program.Instructions[3].Control);
Assert.True(load.UsesFlatAddress);
Assert.Equal(uint.MaxValue, load.ScalarAddress);
}
[Fact]
public void SadU32CompilesToUnsignedMinMaxDifferenceAndAdd()
{
var program = DecodeProgram(0xD15D0003u, 0x040A0300u, SEndpgm);
var addition = program.Instructions[0];
Assert.Equal("VSadU32", addition.Opcode);
Assert.Equal([Gen5Operand.Vector(0), Gen5Operand.Vector(1), Gen5Operand.Vector(2)], addition.Sources);
Assert.Equal([Gen5Operand.Vector(3)], addition.Destinations);
var request = ResourceTestProgram.Request(program, userDataCount: 0);
Assert.True(
Gen5SpirvTranslator.TryCompileProgram(
request,
out var compiled,
out var error),
error);
var opcodes = ReadSpirvOpcodes(compiled.Spirv);
Assert.Contains((ushort)SpirvOp.ExtInst, opcodes);
Assert.Contains((ushort)SpirvOp.ISub, opcodes);
Assert.Contains((ushort)SpirvOp.IAdd, opcodes);
var extendedOperations = new List<uint>();
for (var offset = 5 * sizeof(uint); offset < compiled.Spirv.Length;)
{
var header = BinaryPrimitives.ReadUInt32LittleEndian(compiled.Spirv.AsSpan(offset));
if ((ushort)header == (ushort)SpirvOp.ExtInst)
{
extendedOperations.Add(BinaryPrimitives.ReadUInt32LittleEndian(compiled.Spirv.AsSpan(offset + 4 * sizeof(uint))));
}
offset += checked((int)(header >> 16) * sizeof(uint));
}
Assert.Contains(38u, extendedOperations);
Assert.Contains(41u, extendedOperations);
}
public static TheoryData<uint, string> F16CompareOpcodes => new()
{
{ 0xC8, "VCmpFF16" },
{ 0xC9, "VCmpLtF16" },
{ 0xCA, "VCmpEqF16" },
{ 0xCB, "VCmpLeF16" },
{ 0xCC, "VCmpGtF16" },
{ 0xCD, "VCmpLgF16" },
{ 0xCE, "VCmpGeF16" },
{ 0xCF, "VCmpOF16" },
{ 0xD8, "VCmpxFF16" },
{ 0xD9, "VCmpxLtF16" },
{ 0xDA, "VCmpxEqF16" },
{ 0xDB, "VCmpxLeF16" },
{ 0xDC, "VCmpxGtF16" },
{ 0xDD, "VCmpxLgF16" },
{ 0xDE, "VCmpxGeF16" },
{ 0xDF, "VCmpxOF16" },
{ 0xE8, "VCmpUF16" },
{ 0xE9, "VCmpNgeF16" },
{ 0xEA, "VCmpNlgF16" },
{ 0xEB, "VCmpNgtF16" },
{ 0xEC, "VCmpNleF16" },
{ 0xED, "VCmpNeqF16" },
{ 0xEE, "VCmpNltF16" },
{ 0xEF, "VCmpTruF16" },
{ 0xF8, "VCmpxUF16" },
{ 0xF9, "VCmpxNgeF16" },
{ 0xFA, "VCmpxNlgF16" },
{ 0xFB, "VCmpxNgtF16" },
{ 0xFC, "VCmpxNleF16" },
{ 0xFD, "VCmpxNeqF16" },
{ 0xFE, "VCmpxNltF16" },
{ 0xFF, "VCmpxTruF16" },
};
[Theory]
[MemberData(nameof(F16CompareOpcodes))]
public void DecodesF16CompareOpcodes(uint opcode, string expected)
{
var program = DecodeProgram(
(0x3Eu << 25) | (opcode << 17) | (1u << 9) | 256u,
SEndpgm);
Assert.Equal(expected, program.Instructions[0].Opcode);
Assert.Equal(
[Gen5Operand.Vector(0), Gen5Operand.Vector(1)],
program.Instructions[0].Sources);
}
[Fact]
public void F16CompareCompilesToOrderedSpirv()
{
var program = DecodeProgram(
(0x3Eu << 25) | (0xC9u << 17) | (1u << 9) | 256u,
SEndpgm);
var request = ResourceTestProgram.Request(program, userDataCount: 0);
Assert.True(
Gen5SpirvTranslator.TryCompileProgram(
request,
out var compiled,
out var error),
error);
Assert.Contains(
(ushort)SpirvOp.FOrdLessThan,
ReadSpirvOpcodes(compiled.Spirv));
}
[Fact]
public void FlatLoadUbyteInfersScalarBaseAndCompiles()
{
var memory = new TestCpuMemory(ShaderAddress, 0x4000);
uint[] words =
[
// v_add_co_u32 v1, vcc_lo, s12, v6
0xD70F6A01,
0x00020C0C,
// v_add_co_ci_u32_sdwa v2, vcc_lo, 0, s13, vcc_lo
0x50041AF9,
0x86860680,
// flat_load_ubyte v0, v[1:2]
0xDC200000,
0x007D0001,
SEndpgm,
];
var shader = new byte[words.Length * sizeof(uint)];
for (var index = 0; index < words.Length; index++)
{
BinaryPrimitives.WriteUInt32LittleEndian(
shader.AsSpan(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 instruction = Assert.Single(
program.Instructions,
item => item.Opcode == "FlatLoadUbyte");
var control = Assert.IsType<Gen5GlobalMemoryControl>(
instruction.Control);
Assert.True(control.UsesFlatAddress);
Assert.Equal(1u, control.VectorAddress);
Assert.Equal(0u, control.SourceVectorRegister);
Assert.Equal(0u, control.DestinationVectorRegister);
Assert.Equal(12u, control.ScalarAddress);
Assert.Equal(
[
Gen5Operand.Vector(1),
Gen5Operand.Vector(2),
Gen5Operand.Scalar(12),
],
instruction.Sources);
uint[] userData =
[
unchecked((uint)ShaderAddress),
unchecked((uint)(ShaderAddress >> 32)),
];
var (plan, resources, layout) = ResourceTestProgram.Prepare(program, userDataBase: 12, userDataCount: 2);
Assert.True(plan.Info.UsesDeviceAddresses);
var range = Assert.Single(plan.DeviceAddressRanges);
Assert.Contains(range.MemoryIndices, index => plan.Memory[index].Pc == instruction.Pc);
var resolved = Assert.Single(DeviceAddressRangePlanner.Evaluate(plan, ResourceTestProgram.Inputs(userData)));
Assert.True(resolved.Planned);
Assert.Equal(ShaderAddress, resolved.Base);
var request = new ShaderCompileRequest(plan, resources, layout);
Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var compiled, out var compileError), compileError);
Assert.Contains((ushort)SpirvOp.ConvertUToPtr, ReadSpirvOpcodes(compiled.Spirv));
}
private static IReadOnlyList<ushort> ReadSpirvOpcodes(byte[] spirv)
{
Assert.Equal(0, spirv.Length % sizeof(uint));
Assert.True(spirv.Length >= 5 * sizeof(uint));
Assert.Equal(
0x07230203u,
BinaryPrimitives.ReadUInt32LittleEndian(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.InRange(
wordCount,
1,
(spirv.Length - offset) / sizeof(uint));
opcodes.Add((ushort)instruction);
offset += wordCount * sizeof(uint);
}
return opcodes;
}
private static Gen5ShaderProgram DecodeProgram(params uint[] words)
{
var memory = new TestCpuMemory(ShaderAddress, 0x4000);
var shader = new byte[words.Length * sizeof(uint)];
for (var index = 0; index < words.Length; index++)
{
BinaryPrimitives.WriteUInt32LittleEndian(
shader.AsSpan(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 error),
error);
return program;
}
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;
}
}
}