358 lines
14 KiB
C#
358 lines
14 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.Tests.Resources;
|
|
using SharpEmu.ShaderCompiler.Vulkan;
|
|
using Xunit;
|
|
|
|
namespace SharpEmu.ShaderCompiler.Tests;
|
|
|
|
public sealed class Gen5Float16ArithmeticTests
|
|
{
|
|
private const ulong ShaderAddress = 0x1_0000_0000;
|
|
private const uint SEndpgm = 0xBF810000;
|
|
|
|
[Theory]
|
|
[InlineData(false)]
|
|
[InlineData(true)]
|
|
public void PackedTruncation_PointerMatchesRegisterStorage(bool elideExecGuards)
|
|
{
|
|
var program = Decode([0x5E101B0Du, SEndpgm]); // v_cvt_pkrtz_f16_f32 v8, v13, v13
|
|
var (plan, resources, bindings) = ResourceTestProgram.Prepare(program, userDataCount: 0);
|
|
var request = new ShaderCompileRequest(plan, resources, bindings)
|
|
{ EnableExecGuardElision = elideExecGuards };
|
|
Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error);
|
|
// The packed-half shadow of v8 is one variable; its pointer type must carry the storage
|
|
// class it is declared with (Function when the register files live in the function).
|
|
var pointers = new Dictionary<uint, (uint Storage, uint Pointee)>();
|
|
var vec2Types = new HashSet<uint>();
|
|
var packedHalfVariables = new List<(uint Type, uint Storage)>();
|
|
for (var offset = 20; offset < shader.Spirv.Length;)
|
|
{
|
|
uint Word(int index) => BinaryPrimitives.ReadUInt32LittleEndian(shader.Spirv.AsSpan(offset + index * 4));
|
|
var header = Word(0);
|
|
var opcode = (SpirvOp)(header & 0xFFFF);
|
|
if (opcode == SpirvOp.TypeVector && Word(3) == 2) vec2Types.Add(Word(1));
|
|
if (opcode == SpirvOp.TypePointer) pointers.Add(Word(1), (Word(2), Word(3)));
|
|
if (opcode == SpirvOp.Variable && vec2Types.Contains(pointers[Word(1)].Pointee))
|
|
packedHalfVariables.Add((Word(1), Word(3)));
|
|
offset += (int)(header >> 16) * 4;
|
|
}
|
|
Assert.NotEmpty(packedHalfVariables);
|
|
foreach (var variable in packedHalfVariables)
|
|
Assert.Equal(variable.Storage, pointers[variable.Type].Storage);
|
|
Gen5LargeDispatcherValidationTests.ValidateWithSpirvToolsWhenAvailable(shader.Spirv);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(false)]
|
|
[InlineData(true)]
|
|
public void PackedFma_WithExactNativeHalfSupport_DoesNotExpandRoundToOdd(bool fmac)
|
|
{
|
|
var program = fmac
|
|
? Decode([0x78000501u, SEndpgm])
|
|
: Decode([0xCC0E4000u, 257u | (258u << 9) | (259u << 18) | (3u << 27), SEndpgm]);
|
|
var (plan, resources, bindings) = ResourceTestProgram.Prepare(program, userDataCount: 0);
|
|
var original = new SharpEmu.ShaderCompiler.ShaderCompileRequest(plan, resources, bindings);
|
|
var request = new SharpEmu.ShaderCompiler.ShaderCompileRequest(plan,
|
|
resources, bindings) { SupportsExactFloat16Conversions = true };
|
|
Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var native, out var error), error);
|
|
Assert.True(Gen5SpirvTranslator.TryCompileProgram(original, out var fallback, out error), error);
|
|
Gen5LargeDispatcherValidationTests.ValidateWithSpirvToolsWhenAvailable(native.Spirv);
|
|
Assert.DoesNotContain((ushort)SpirvOp.FSub, ReadOpcodes(native.Spirv));
|
|
Assert.Contains((ushort)SpirvOp.FSub, ReadOpcodes(fallback.Spirv));
|
|
}
|
|
|
|
[Fact]
|
|
public void CompactFloat16ArithmeticDecodesAndCompilesWithoutNativeFloat16()
|
|
{
|
|
var program = Decode(
|
|
[
|
|
0x7E00AD01, // v_rsq_f16 v0, v1
|
|
0x64000501, // v_add_f16 v0, v1, v2
|
|
0x66060B04, // v_sub_f16 v3, v4, v5
|
|
0x680C1107, // v_subrev_f16 v6, v7, v8
|
|
0x6A12170A, // v_mul_f16 v9, v10, v11
|
|
0x72181D0D, // v_max_f16 v12, v13, v14
|
|
0x741E2310, // v_min_f16 v15, v16, v17
|
|
SEndpgm,
|
|
]);
|
|
|
|
Assert.Equal(
|
|
["VRsqF16", "VAddF16", "VSubF16", "VSubrevF16", "VMulF16", "VMaxF16", "VMinF16", "SEndpgm"],
|
|
program.Instructions.Select(instruction => instruction.Opcode));
|
|
|
|
var request = ResourceTestProgram.Request(program, userDataCount: 0);
|
|
|
|
Assert.True(
|
|
Gen5SpirvTranslator.TryCompileProgram(
|
|
request,
|
|
out var shader,
|
|
out var error),
|
|
error);
|
|
|
|
var opcodes = ReadOpcodes(shader.Spirv);
|
|
Assert.Contains((ushort)SpirvOp.FAdd, opcodes);
|
|
Assert.Contains((ushort)SpirvOp.FSub, opcodes);
|
|
Assert.Contains((ushort)SpirvOp.FMul, opcodes);
|
|
Assert.True(opcodes.Count(opcode => opcode == (ushort)SpirvOp.ExtInst) >= 3);
|
|
Assert.DoesNotContain((ushort)SpirvCapability.Float16, ReadCapabilities(shader.Spirv));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0x54u, "VRcpF16")]
|
|
[InlineData(0x55u, "VSqrtF16")]
|
|
[InlineData(0x57u, "VLogF16")]
|
|
[InlineData(0x58u, "VExpF16")]
|
|
[InlineData(0x5Bu, "VFloorF16")]
|
|
[InlineData(0x5Cu, "VCeilF16")]
|
|
[InlineData(0x5Du, "VTruncF16")]
|
|
[InlineData(0x5Eu, "VRndneF16")]
|
|
[InlineData(0x5Fu, "VFractF16")]
|
|
[InlineData(0x60u, "VSinF16")]
|
|
[InlineData(0x61u, "VCosF16")]
|
|
public void Float16UnaryDecodesAndCompilesInBothEncodings(uint opcode, string name)
|
|
{
|
|
// VOP1 v0, v1 (Silent Hill f: v_log_f16), then the VOP3 form v2, v3.
|
|
var program = Decode(
|
|
[
|
|
0x7E000000u | (opcode << 9) | 257u,
|
|
(0x35u << 26) | ((0x180u + opcode) << 16) | 2u,
|
|
259u,
|
|
SEndpgm,
|
|
]);
|
|
|
|
Assert.Equal([name, name, "SEndpgm"], program.Instructions.Select(instruction => instruction.Opcode));
|
|
var request = ResourceTestProgram.Request(program, userDataCount: 0);
|
|
Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error);
|
|
Assert.DoesNotContain((ushort)SpirvCapability.Float16, ReadCapabilities(shader.Spirv));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0x351u, "VMin3F16")]
|
|
[InlineData(0x354u, "VMax3F16")]
|
|
[InlineData(0x357u, "VMed3F16")]
|
|
public void Vop3Float16ThreeOperandMinMaxDecodesAndCompiles(uint opcode, string name)
|
|
{
|
|
// Silent Hill's pixel shaders use v_min3_f16.
|
|
var program = Decode(
|
|
[
|
|
(0x35u << 26) | (opcode << 16) | 122u,
|
|
261u | (262u << 9) | (263u << 18),
|
|
SEndpgm,
|
|
]);
|
|
|
|
Assert.Equal(name, program.Instructions[0].Opcode);
|
|
var request = ResourceTestProgram.Request(program, userDataCount: 0);
|
|
Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error);
|
|
Assert.DoesNotContain((ushort)SpirvCapability.Float16, ReadCapabilities(shader.Spirv));
|
|
}
|
|
|
|
[Fact]
|
|
public void Vop3Float16FmaDecodesAndCompiles()
|
|
{
|
|
var program = Decode(
|
|
[
|
|
(0x35u << 26) | (0x34Bu << 16) | 122u,
|
|
261u | (262u << 9) | (263u << 18),
|
|
SEndpgm,
|
|
]);
|
|
|
|
Assert.Equal("VFmaF16", program.Instructions[0].Opcode);
|
|
var request = ResourceTestProgram.Request(program, userDataCount: 0);
|
|
Assert.True(
|
|
Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error),
|
|
error);
|
|
Assert.Contains((ushort)SpirvOp.ExtInst, ReadOpcodes(shader.Spirv));
|
|
Assert.DoesNotContain((ushort)SpirvCapability.Float16, ReadCapabilities(shader.Spirv));
|
|
}
|
|
|
|
[Fact]
|
|
public void PackedFloat16MultiplyAcceptsLiteralOperand()
|
|
{
|
|
var program = Decode(
|
|
[
|
|
(0x33u << 26) | (0x10u << 16) | 1u,
|
|
0xFFu | (258u << 9),
|
|
0x00002C00u,
|
|
SEndpgm,
|
|
]);
|
|
|
|
Assert.Equal("VPkMulF16", program.Instructions[0].Opcode);
|
|
Assert.Equal(Gen5OperandKind.LiteralConstant, program.Instructions[0].Sources[0].Kind);
|
|
Assert.Equal(0x00002C00u, program.Instructions[0].Sources[0].Value);
|
|
var request = ResourceTestProgram.Request(program, userDataCount: 0);
|
|
Assert.True(
|
|
Gen5SpirvTranslator.TryCompileProgram(request, out _, out var error),
|
|
error);
|
|
}
|
|
|
|
// The native GLSL conversions are used only when the host measured them bit-exact on the
|
|
// device; otherwise the branchless integer sequences (FindUMsb = GLSL 75) stay in place.
|
|
[Theory]
|
|
[InlineData(false)]
|
|
[InlineData(true)]
|
|
public void PackedFloat16UsesNativeHalfConversionOnlyWhenTheHostMeasuredItExact(bool nativeExact)
|
|
{
|
|
var program = Decode(
|
|
[
|
|
(0x33u << 26) | (0x10u << 16) | 1u,
|
|
0xFFu | (258u << 9),
|
|
0x00002C00u,
|
|
SEndpgm,
|
|
]);
|
|
|
|
Assert.Equal("VPkMulF16", program.Instructions[0].Opcode);
|
|
var (plan, resources, layout) = ResourceTestProgram.Prepare(program, SharpEmu.ShaderCompiler.Resources.ShaderStage.Compute, 0, 0);
|
|
var request = new ShaderCompileRequest(plan, resources, layout)
|
|
{
|
|
NativeHalfConversionExact = nativeExact,
|
|
};
|
|
|
|
Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error);
|
|
var glsl = ReadGlslExtOpcodes(shader.Spirv);
|
|
|
|
// 62 = UnpackHalf2x16, 58 = PackHalf2x16, 75 = FindUMsb.
|
|
if (nativeExact)
|
|
{
|
|
Assert.Contains(62u, glsl);
|
|
Assert.Contains(58u, glsl);
|
|
}
|
|
else
|
|
{
|
|
Assert.DoesNotContain(62u, glsl);
|
|
Assert.DoesNotContain(58u, glsl);
|
|
Assert.Contains(75u, glsl);
|
|
}
|
|
}
|
|
|
|
// The GLSL.std.450 instruction number of every ExtInst in the module.
|
|
private static IReadOnlyList<uint> ReadGlslExtOpcodes(byte[] spirv)
|
|
{
|
|
var opcodes = new List<uint>();
|
|
foreach (var words in ReadInstructionWords(spirv))
|
|
{
|
|
if ((ushort)words[0] == (ushort)SpirvOp.ExtInst && words.Length >= 5)
|
|
{
|
|
opcodes.Add(words[4]);
|
|
}
|
|
}
|
|
|
|
return opcodes;
|
|
}
|
|
|
|
private static IEnumerable<uint[]> ReadInstructionWords(byte[] spirv)
|
|
{
|
|
Assert.Equal(0x07230203u, BinaryPrimitives.ReadUInt32LittleEndian(spirv));
|
|
for (var offset = 5 * sizeof(uint); offset < spirv.Length;)
|
|
{
|
|
var header = BinaryPrimitives.ReadUInt32LittleEndian(spirv.AsSpan(offset));
|
|
var wordCount = checked((int)(header >> 16));
|
|
Assert.InRange(wordCount, 1, (spirv.Length - offset) / sizeof(uint));
|
|
var words = new uint[wordCount];
|
|
for (var index = 0; index < wordCount; index++)
|
|
{
|
|
words[index] = BinaryPrimitives.ReadUInt32LittleEndian(spirv.AsSpan(offset + index * sizeof(uint)));
|
|
}
|
|
|
|
yield return words;
|
|
offset += wordCount * sizeof(uint);
|
|
}
|
|
}
|
|
|
|
public static Gen5ShaderProgram Decode(IReadOnlyList<uint> words)
|
|
{
|
|
var memory = new TestCpuMemory(ShaderAddress, words.Count * sizeof(uint));
|
|
var bytes = new byte[words.Count * sizeof(uint)];
|
|
for (var index = 0; index < words.Count; index++)
|
|
{
|
|
BinaryPrimitives.WriteUInt32LittleEndian(
|
|
bytes.AsSpan(index * sizeof(uint)),
|
|
words[index]);
|
|
}
|
|
|
|
Assert.True(memory.TryWrite(ShaderAddress, bytes));
|
|
var context = new CpuContext(memory, Generation.Gen5);
|
|
Assert.True(
|
|
Gen5ShaderTranslator.TryDecodeProgram(
|
|
context,
|
|
ShaderAddress,
|
|
out var program,
|
|
out var error),
|
|
error);
|
|
return program;
|
|
}
|
|
|
|
private static IReadOnlyList<ushort> ReadOpcodes(byte[] spirv) =>
|
|
ReadInstructions(spirv)
|
|
.Select(instruction => instruction.Opcode)
|
|
.ToArray();
|
|
|
|
private static IReadOnlyList<ushort> ReadCapabilities(byte[] spirv) =>
|
|
ReadInstructions(spirv)
|
|
.Where(instruction => instruction.Opcode == (ushort)SpirvOp.Capability)
|
|
.Select(instruction => (ushort)instruction.FirstOperand)
|
|
.ToArray();
|
|
|
|
private static IReadOnlyList<(ushort Opcode, uint FirstOperand)> ReadInstructions(
|
|
byte[] spirv)
|
|
{
|
|
Assert.Equal(0x07230203u, BinaryPrimitives.ReadUInt32LittleEndian(spirv));
|
|
var instructions = new List<(ushort Opcode, uint FirstOperand)>();
|
|
for (var offset = 5 * sizeof(uint); offset < spirv.Length;)
|
|
{
|
|
var header = BinaryPrimitives.ReadUInt32LittleEndian(spirv.AsSpan(offset));
|
|
var wordCount = checked((int)(header >> 16));
|
|
Assert.InRange(wordCount, 1, (spirv.Length - offset) / sizeof(uint));
|
|
var firstOperand = wordCount > 1
|
|
? BinaryPrimitives.ReadUInt32LittleEndian(spirv.AsSpan(offset + sizeof(uint)))
|
|
: 0;
|
|
instructions.Add(((ushort)header, firstOperand));
|
|
offset += wordCount * sizeof(uint);
|
|
}
|
|
|
|
return instructions;
|
|
}
|
|
|
|
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 address, int length, out int offset)
|
|
{
|
|
offset = 0;
|
|
if (address < baseAddress || address - baseAddress > int.MaxValue)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
offset = (int)(address - baseAddress);
|
|
return offset <= _storage.Length - length;
|
|
}
|
|
}
|
|
}
|