mirror of
https://github.com/par274/sharpemu.git
synced 2026-08-30 21:31:57 +08:00
shader: add compact f16 arithmetic and compare lowering (#840)
Co-authored-by: Foued Attar <attar.foued@gmail.com>
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.ShaderCompiler;
|
||||
using SharpEmu.ShaderCompiler.Metal;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.ShaderCompiler.Metal.Tests;
|
||||
|
||||
public sealed class Gen5MslF16CompareTests
|
||||
{
|
||||
public static TheoryData<string> Opcodes = new()
|
||||
{
|
||||
"VCmpFF16",
|
||||
"VCmpLtF16",
|
||||
"VCmpEqF16",
|
||||
"VCmpLeF16",
|
||||
"VCmpGtF16",
|
||||
"VCmpLgF16",
|
||||
"VCmpGeF16",
|
||||
"VCmpOF16",
|
||||
"VCmpxFF16",
|
||||
"VCmpxLtF16",
|
||||
"VCmpxEqF16",
|
||||
"VCmpxLeF16",
|
||||
"VCmpxGtF16",
|
||||
"VCmpxLgF16",
|
||||
"VCmpxGeF16",
|
||||
"VCmpxOF16",
|
||||
"VCmpUF16",
|
||||
"VCmpNgeF16",
|
||||
"VCmpNlgF16",
|
||||
"VCmpNgtF16",
|
||||
"VCmpNleF16",
|
||||
"VCmpNeqF16",
|
||||
"VCmpNltF16",
|
||||
"VCmpTruF16",
|
||||
"VCmpxUF16",
|
||||
"VCmpxNgeF16",
|
||||
"VCmpxNlgF16",
|
||||
"VCmpxNgtF16",
|
||||
"VCmpxNleF16",
|
||||
"VCmpxNeqF16",
|
||||
"VCmpxNltF16",
|
||||
"VCmpxTruF16",
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(Opcodes))]
|
||||
public void F16CompareOpcodeLowersToMsl(string opcode)
|
||||
{
|
||||
var compare = new Gen5ShaderInstruction(
|
||||
0,
|
||||
Gen5ShaderEncoding.Vopc,
|
||||
opcode,
|
||||
[0u],
|
||||
[Gen5Operand.Vector(0), Gen5Operand.Vector(1)],
|
||||
[],
|
||||
null);
|
||||
var state = new Gen5ShaderState(
|
||||
new Gen5ShaderProgram(0x1000, [compare]),
|
||||
[],
|
||||
null);
|
||||
var scalars = new uint[256];
|
||||
var evaluation = new Gen5ShaderEvaluation(scalars, scalars, [], []);
|
||||
|
||||
Assert.True(
|
||||
Gen5MslTranslator.TryCompileComputeShader(
|
||||
state,
|
||||
evaluation,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
out var shader,
|
||||
out var error),
|
||||
error);
|
||||
Assert.NotEmpty(shader.Source);
|
||||
if (opcode is not (
|
||||
"VCmpFF16" or "VCmpxFF16" or
|
||||
"VCmpTruF16" or "VCmpxTruF16"))
|
||||
{
|
||||
Assert.Contains("half", shader.Source, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.ShaderCompiler.Metal.Tests;
|
||||
|
||||
public sealed class MslFloat16ArithmeticTests
|
||||
{
|
||||
[Fact]
|
||||
public void CompactFloat16ArithmeticUsesHalfOperandsAndPreservesRegisterShape()
|
||||
{
|
||||
var fixture = new Gen5ComputeFixture(
|
||||
"compact-f16-arithmetic",
|
||||
[
|
||||
0x64000501,
|
||||
0x66060B04,
|
||||
0x680C1107,
|
||||
0x6A12170A,
|
||||
0x72181D0D,
|
||||
0x741E2310,
|
||||
0xBF810000,
|
||||
],
|
||||
StoreScalarResourceBase: 0,
|
||||
StoreBackingBytes: 0);
|
||||
|
||||
var shader = Gen5ComputeFixtures.CompileOrThrow(fixture);
|
||||
|
||||
Assert.Contains("as_type<half>", shader.Source, StringComparison.Ordinal);
|
||||
Assert.Contains("fmin(", shader.Source, StringComparison.Ordinal);
|
||||
Assert.Contains("fmax(", shader.Source, StringComparison.Ordinal);
|
||||
Assert.Contains("& 0xFFFF0000u", shader.Source, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.HLE;
|
||||
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;
|
||||
|
||||
[Fact]
|
||||
public void CompactFloat16ArithmeticDecodesAndCompilesWithoutNativeFloat16()
|
||||
{
|
||||
var program = Decode(
|
||||
[
|
||||
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(
|
||||
["VAddF16", "VSubF16", "VSubrevF16", "VMulF16", "VMaxF16", "VMinF16", "SEndpgm"],
|
||||
program.Instructions.Select(instruction => instruction.Opcode));
|
||||
|
||||
var state = new Gen5ShaderState(program, [], null);
|
||||
var scalarRegisters = new uint[256];
|
||||
var evaluation = new Gen5ShaderEvaluation(
|
||||
scalarRegisters,
|
||||
scalarRegisters,
|
||||
[],
|
||||
[]);
|
||||
|
||||
Assert.True(
|
||||
Gen5SpirvTranslator.TryCompileComputeShader(
|
||||
state,
|
||||
evaluation,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
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) >= 2);
|
||||
Assert.DoesNotContain((ushort)SpirvCapability.Float16, ReadCapabilities(shader.Spirv));
|
||||
}
|
||||
|
||||
private 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
// 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.Vulkan;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.ShaderCompiler.Tests;
|
||||
|
||||
public sealed class Gen5VopcF16Tests
|
||||
{
|
||||
private const ulong ShaderAddress = 0x1_0000_0000;
|
||||
private const uint SEndpgm = 0xBF810000;
|
||||
|
||||
public static TheoryData<uint, string> Opcodes = 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(Opcodes))]
|
||||
public void F16CompareOpcodeDecodes(uint opcode, string expectedName)
|
||||
{
|
||||
var memory = new TestCpuMemory(ShaderAddress, 0x100);
|
||||
Span<byte> shader = stackalloc byte[2 * sizeof(uint)];
|
||||
var word = (0x3Eu << 25) | (opcode << 17) | (1u << 9);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(shader, word);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(shader[sizeof(uint)..], SEndpgm);
|
||||
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);
|
||||
var instruction = Assert.Single(
|
||||
program.Instructions,
|
||||
candidate => candidate.Encoding == Gen5ShaderEncoding.Vopc);
|
||||
Assert.Equal(expectedName, instruction.Opcode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(Opcodes))]
|
||||
public void F16CompareOpcodeLowersToSpirv(uint _, string opcode)
|
||||
{
|
||||
var compare = new Gen5ShaderInstruction(
|
||||
0,
|
||||
Gen5ShaderEncoding.Vopc,
|
||||
opcode,
|
||||
[0u],
|
||||
[Gen5Operand.Vector(0), Gen5Operand.Vector(1)],
|
||||
[],
|
||||
null);
|
||||
var state = new Gen5ShaderState(
|
||||
new Gen5ShaderProgram(ShaderAddress, [compare]),
|
||||
[],
|
||||
null);
|
||||
var scalars = new uint[256];
|
||||
var evaluation = new Gen5ShaderEvaluation(scalars, scalars, [], []);
|
||||
|
||||
Assert.True(
|
||||
Gen5SpirvTranslator.TryCompileComputeShader(
|
||||
state,
|
||||
evaluation,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
out var shader,
|
||||
out var error),
|
||||
error);
|
||||
Assert.NotEmpty(shader.Spirv);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user