diff --git a/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.Alu.cs b/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.Alu.cs index 0907f208..d9566996 100644 --- a/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.Alu.cs +++ b/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.Alu.cs @@ -144,11 +144,35 @@ public static partial class Gen5MslTranslator // ---- float arithmetic ---- "VAddF32" => FloatResult(instruction, $"{F(instruction, 0)} + {F(instruction, 1)}"), + "VAddF16" => Float16Result( + instruction, + destination, + $"{F16(instruction, 0)} + {F16(instruction, 1)}"), "VSubF32" => FloatResult(instruction, $"{F(instruction, 0)} - {F(instruction, 1)}"), "VSubrevF32" => FloatResult(instruction, $"{F(instruction, 1)} - {F(instruction, 0)}"), + "VSubF16" => Float16Result( + instruction, + destination, + $"{F16(instruction, 0)} - {F16(instruction, 1)}"), + "VSubrevF16" => Float16Result( + instruction, + destination, + $"{F16(instruction, 1)} - {F16(instruction, 0)}"), "VMulF32" => FloatResult(instruction, $"{F(instruction, 0)} * {F(instruction, 1)}"), + "VMulF16" => Float16Result( + instruction, + destination, + $"{F16(instruction, 0)} * {F16(instruction, 1)}"), "VMinF32" => FloatResult(instruction, $"fmin({F(instruction, 0)}, {F(instruction, 1)})"), "VMaxF32" => FloatResult(instruction, $"fmax({F(instruction, 0)}, {F(instruction, 1)})"), + "VMinF16" => Float16Result( + instruction, + destination, + $"fmin({F16(instruction, 0)}, {F16(instruction, 1)})"), + "VMaxF16" => Float16Result( + instruction, + destination, + $"fmax({F16(instruction, 0)}, {F16(instruction, 1)})"), // The decoder normalizes mk/ak literal placement, so every MAD/FMA // form is fma(src0, src1, src2) exactly like the SPIR-V translator. "VFmaF32" or "VMadF32" or "VMadAkF32" or "VMadMkF32" or "VFmaAkF32" or "VFmaMkF32" => @@ -578,23 +602,46 @@ public static partial class Gen5MslTranslator { condition = EmitCompareClass(instruction); } - else if (opcode is "VCmpTruF32" or "VCmpxTruF32" or "VCmpTI32" or "VCmpTU32") + else if (opcode is + "VCmpTruF32" or "VCmpxTruF32" or + "VCmpTruF16" or "VCmpxTruF16" or + "VCmpTI32" or "VCmpTU32") { condition = "true"; } - else if (opcode is "VCmpFF32" or "VCmpxFF32" or "VCmpFI32" or "VCmpFU32") + else if (opcode is + "VCmpFF32" or "VCmpxFF32" or + "VCmpFF16" or "VCmpxFF16" or + "VCmpFI32" or "VCmpFU32") { condition = "false"; } - else if (opcode is "VCmpOF32" or "VCmpxOF32") + else if (opcode is + "VCmpOF32" or "VCmpxOF32" or + "VCmpOF16" or "VCmpxOF16") { - condition = $"(!isnan({F(instruction, 0)}) && !isnan({F(instruction, 1)}))"; + var left = opcode.EndsWith("F16", StringComparison.Ordinal) + ? F16(instruction, 0) + : F(instruction, 0); + var right = opcode.EndsWith("F16", StringComparison.Ordinal) + ? F16(instruction, 1) + : F(instruction, 1); + condition = $"(!isnan({left}) && !isnan({right}))"; } - else if (opcode is "VCmpUF32" or "VCmpxUF32") + else if (opcode is + "VCmpUF32" or "VCmpxUF32" or + "VCmpUF16" or "VCmpxUF16") { - condition = $"(isnan({F(instruction, 0)}) || isnan({F(instruction, 1)}))"; + var left = opcode.EndsWith("F16", StringComparison.Ordinal) + ? F16(instruction, 0) + : F(instruction, 0); + var right = opcode.EndsWith("F16", StringComparison.Ordinal) + ? F16(instruction, 1) + : F(instruction, 1); + condition = $"(isnan({left}) || isnan({right}))"; } - else if (opcode.EndsWith("F32", StringComparison.Ordinal)) + else if (opcode.EndsWith("F32", StringComparison.Ordinal) || + opcode.EndsWith("F16", StringComparison.Ordinal)) { // Ordered compares are the plain C operators (false on NaN); // the Nxx forms are their unordered negations (true on NaN). @@ -620,7 +667,13 @@ public static partial class Gen5MslTranslator return false; } - var comparison = $"({F(instruction, 0)} {op} {F(instruction, 1)})"; + var left = opcode.EndsWith("F16", StringComparison.Ordinal) + ? F16(instruction, 0) + : F(instruction, 0); + var right = opcode.EndsWith("F16", StringComparison.Ordinal) + ? F16(instruction, 1) + : F(instruction, 1); + var comparison = $"({left} {op} {right})"; condition = unordered ? $"(!{comparison})" : comparison; } else @@ -1570,6 +1623,80 @@ public static partial class Gen5MslTranslator return expression; } + /// Reads the selected 16-bit half as a widened float. + private string F16(Gen5ShaderInstruction instruction, int sourceIndex) + { + var operand = instruction.Sources[sourceIndex]; + string expression; + if (operand.Kind == Gen5OperandKind.EncodedConstant && + Gen5InlineConstants.TryDecode(operand.Value, out var inline)) + { + expression = operand.Value switch + { + >= 128 and <= 192 => $"{operand.Value - 128}.0f", + >= 193 and <= 208 => $"(-{operand.Value - 192}.0f)", + _ => AsFloat(FormatUInt(inline)), + }; + } + else + { + var raw = RawSource( + instruction, + sourceIndex, + applySdwaIntegerModifiers: false); + var shift = instruction.Control is Gen5Vop3Control control && + (control.OperandSelect & (1u << sourceIndex)) != 0 + ? 16 + : 0; + expression = + $"(float)as_type((ushort)((({raw}) >> {shift}) & 0xFFFFu))"; + } + + var (absoluteMask, negateMask) = instruction.Control switch + { + Gen5Vop3Control control => (control.AbsoluteMask, control.NegateMask), + Gen5SdwaControl control => (control.AbsoluteMask, control.NegateMask), + Gen5DppControl control => (control.AbsoluteMask, control.NegateMask), + _ => (0u, 0u), + }; + if ((absoluteMask & (1u << sourceIndex)) != 0) + { + expression = $"fabs({expression})"; + } + + if ((negateMask & (1u << sourceIndex)) != 0) + { + expression = $"(-{expression})"; + } + + return expression; + } + + /// Rounds to f16 and preserves the unselected VGPR half. + private string Float16Result( + Gen5ShaderInstruction instruction, + uint destination, + string expression) + { + var control = instruction.Control as Gen5Vop3Control; + expression = (control?.OutputModifier ?? 0) switch + { + 1 => $"(({expression}) * 2.0f)", + 2 => $"(({expression}) * 4.0f)", + 3 => $"(({expression}) * 0.5f)", + _ => expression, + }; + if (control?.Clamp == true) + { + expression = $"clamp({expression}, 0.0f, 1.0f)"; + } + + var packed = $"(uint)as_type(half({expression}))"; + return ((control?.OperandSelect ?? 0) & 8) != 0 + ? $"((v[{destination}] & 0x0000FFFFu) | (({packed}) << 16))" + : $"((v[{destination}] & 0xFFFF0000u) | ({packed}))"; + } + /// /// Wraps a float expression with VOP3/SDWA output modifiers and clamp, /// then bitcasts back to the register file's uint domain. diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs index 32e5dd38..d7502cc4 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs @@ -340,21 +340,43 @@ public static partial class Gen5SpirvTranslator case "VAddF32": result = EmitFloatBinary(instruction, SpirvOp.FAdd); break; + case "VAddF16": + result = EmitFloat16Binary(instruction, destination, SpirvOp.FAdd); + break; case "VSubF32": result = EmitFloatBinary(instruction, SpirvOp.FSub); break; case "VSubrevF32": result = EmitFloatBinary(instruction, SpirvOp.FSub, reverse: true); break; + case "VSubF16": + result = EmitFloat16Binary(instruction, destination, SpirvOp.FSub); + break; + case "VSubrevF16": + result = EmitFloat16Binary( + instruction, + destination, + SpirvOp.FSub, + reverse: true); + break; case "VMulF32": result = EmitFloatBinary(instruction, SpirvOp.FMul); break; + case "VMulF16": + result = EmitFloat16Binary(instruction, destination, SpirvOp.FMul); + break; case "VMinF32": result = EmitFloatExtBinary(instruction, 37); break; case "VMaxF32": result = EmitFloatExtBinary(instruction, 40); break; + case "VMinF16": + result = EmitFloat16ExtBinary(instruction, destination, 37); + break; + case "VMaxF16": + result = EmitFloat16ExtBinary(instruction, destination, 40); + break; case "VMadF32": case "VFmaF32": case "VMadMkF32": @@ -1609,29 +1631,72 @@ public static partial class Gen5SpirvTranslator condition, SignedClass(0x020, 0x040, zero)); } - else if (opcode is "VCmpFF32" or "VCmpxFF32" or "VCmpFI32" or "VCmpFU32") + else if (opcode is + "VCmpFF32" or "VCmpxFF32" or + "VCmpFF16" or "VCmpxFF16" or + "VCmpFI32" or "VCmpFU32") { condition = _module.ConstantBool(false); } - else if (opcode is "VCmpTruF32" or "VCmpxTruF32" or "VCmpTI32" or "VCmpTU32") + else if (opcode is + "VCmpTruF32" or "VCmpxTruF32" or + "VCmpTruF16" or "VCmpxTruF16" or + "VCmpTI32" or "VCmpTU32") { condition = _module.ConstantBool(true); } else if (opcode is "VCmpOF32" or "VCmpxOF32" or - "VCmpUF32" or "VCmpxUF32") + "VCmpUF32" or "VCmpxUF32" or + "VCmpOF16" or "VCmpxOF16" or + "VCmpUF16" or "VCmpxUF16") { - var left = GetFloatSource(instruction, 0); - var right = GetFloatSource(instruction, 1); + var isHalf = opcode.EndsWith("F16", StringComparison.Ordinal); + var left = isHalf + ? GetFloat16Source(instruction, 0) + : GetFloatSource(instruction, 0); + var right = isHalf + ? GetFloat16Source(instruction, 1) + : GetFloatSource(instruction, 1); var unordered = _module.AddInstruction( SpirvOp.LogicalOr, _boolType, _module.AddInstruction(SpirvOp.IsNan, _boolType, left), _module.AddInstruction(SpirvOp.IsNan, _boolType, right)); - condition = opcode is "VCmpUF32" or "VCmpxUF32" + condition = opcode is + "VCmpUF32" or "VCmpxUF32" or + "VCmpUF16" or "VCmpxUF16" ? unordered : _module.AddInstruction(SpirvOp.LogicalNot, _boolType, unordered); } + else if (opcode.EndsWith("F16", StringComparison.Ordinal)) + { + var left = GetFloat16Source(instruction, 0); + var right = GetFloat16Source(instruction, 1); + var operation = opcode switch + { + "VCmpLtF16" or "VCmpxLtF16" => SpirvOp.FOrdLessThan, + "VCmpEqF16" or "VCmpxEqF16" => SpirvOp.FOrdEqual, + "VCmpLeF16" or "VCmpxLeF16" => SpirvOp.FOrdLessThanEqual, + "VCmpGtF16" or "VCmpxGtF16" => SpirvOp.FOrdGreaterThan, + "VCmpLgF16" or "VCmpxLgF16" => SpirvOp.FOrdNotEqual, + "VCmpGeF16" or "VCmpxGeF16" => SpirvOp.FOrdGreaterThanEqual, + "VCmpNeqF16" or "VCmpxNeqF16" => SpirvOp.FUnordNotEqual, + "VCmpNltF16" or "VCmpxNltF16" => SpirvOp.FUnordGreaterThanEqual, + "VCmpNleF16" or "VCmpxNleF16" => SpirvOp.FUnordGreaterThan, + "VCmpNgtF16" or "VCmpxNgtF16" => SpirvOp.FUnordLessThanEqual, + "VCmpNgeF16" or "VCmpxNgeF16" => SpirvOp.FUnordLessThan, + "VCmpNlgF16" or "VCmpxNlgF16" => SpirvOp.FUnordEqual, + _ => SpirvOp.Nop, + }; + if (operation == SpirvOp.Nop) + { + error = $"unsupported half compare {opcode}"; + return false; + } + + condition = _module.AddInstruction(operation, _boolType, left, right); + } else if (opcode is not ("VCmpClassF32" or "VCmpxClassF32") && opcode.EndsWith("F32", StringComparison.Ordinal)) { @@ -3108,6 +3173,70 @@ public static partial class Gen5SpirvTranslator sourceAllowsWrite)); } + private uint GetFloat16Source( + Gen5ShaderInstruction instruction, + int sourceIndex) + { + var operand = instruction.Sources[sourceIndex]; + uint value; + if (operand.Kind == Gen5OperandKind.EncodedConstant && + operand.Value is >= 128 and <= 192) + { + value = Float(operand.Value - 128); + } + else if (operand.Kind == Gen5OperandKind.EncodedConstant && + operand.Value is >= 193 and <= 208) + { + value = Float(-(operand.Value - 192)); + } + else if (operand.Kind == Gen5OperandKind.EncodedConstant && + Gen5InlineConstants.TryDecode(operand.Value, out var inline)) + { + value = Bitcast(_floatType, UInt(inline)); + } + else + { + var raw = GetRawSource( + instruction, + sourceIndex, + applySdwaIntegerModifiers: false); + if (instruction.Control is Gen5Vop3Control control && + (control.OperandSelect & (1u << sourceIndex)) != 0) + { + raw = ShiftRightLogical(raw, UInt(16)); + } + + value = Bitcast(_floatType, EmitHalfToFloat(raw)); + } + + uint absoluteMask = 0; + uint negateMask = 0; + switch (instruction.Control) + { + case Gen5Vop3Control control: + absoluteMask = control.AbsoluteMask; + negateMask = control.NegateMask; + break; + case Gen5SdwaControl control: + absoluteMask = control.AbsoluteMask; + negateMask = control.NegateMask; + break; + case Gen5DppControl control: + absoluteMask = control.AbsoluteMask; + negateMask = control.NegateMask; + break; + } + + if ((absoluteMask & (1u << sourceIndex)) != 0) + { + value = Ext(4, _floatType, value); + } + + return (negateMask & (1u << sourceIndex)) != 0 + ? _module.AddInstruction(SpirvOp.FNegate, _floatType, value) + : value; + } + private uint GetFloatSource( Gen5ShaderInstruction instruction, int sourceIndex) @@ -3232,6 +3361,33 @@ public static partial class Gen5SpirvTranslator _module.AddInstruction(SpirvOp.UConvert, _uintType, high)); } + private uint EmitFloat16Binary( + Gen5ShaderInstruction instruction, + uint destination, + SpirvOp operation, + bool reverse = false) + { + var left = GetFloat16Source(instruction, reverse ? 1 : 0); + var right = GetFloat16Source(instruction, reverse ? 0 : 1); + return EmitFloat16Result( + instruction, + destination, + _module.AddInstruction(operation, _floatType, left, right)); + } + + private uint EmitFloat16ExtBinary( + Gen5ShaderInstruction instruction, + uint destination, + uint operation) => + EmitFloat16Result( + instruction, + destination, + Ext( + operation, + _floatType, + GetFloat16Source(instruction, 0), + GetFloat16Source(instruction, 1))); + private uint EmitFloatBinary( Gen5ShaderInstruction instruction, SpirvOp operation, @@ -3753,6 +3909,35 @@ public static partial class Gen5SpirvTranslator UInt(0)); } + private uint EmitFloat16Result( + Gen5ShaderInstruction instruction, + uint destination, + uint value) + { + var control = instruction.Control as Gen5Vop3Control; + value = (control?.OutputModifier ?? 0) switch + { + 1 => _module.AddInstruction(SpirvOp.FMul, _floatType, value, Float(2)), + 2 => _module.AddInstruction(SpirvOp.FMul, _floatType, value, Float(4)), + 3 => _module.AddInstruction(SpirvOp.FMul, _floatType, value, Float(0.5f)), + _ => value, + }; + if (control?.Clamp == true) + { + value = Ext(43, _floatType, value, Float(0), Float(1)); + } + + var half = EmitFloatToHalf(Bitcast(_uintType, value)); + var current = LoadV(destination); + return ((control?.OperandSelect ?? 0) & 8) != 0 + ? BitwiseOr( + BitwiseAnd(current, UInt(0x0000_FFFF)), + ShiftLeftLogical(half, UInt(16))) + : BitwiseOr( + BitwiseAnd(current, UInt(0xFFFF_0000)), + half); + } + private uint EmitFloatResult( Gen5ShaderInstruction instruction, uint value) diff --git a/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs b/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs index d4c77bec..3638bd84 100644 --- a/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs +++ b/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs @@ -1015,6 +1015,12 @@ public static class Gen5ShaderTranslator 0x2F => "VCvtPkrtzF16F32", 0x30 => "VCvtPkU16U32", 0x31 => "VCvtPkI16I32", + 0x32 => "VAddF16", + 0x33 => "VSubF16", + 0x34 => "VSubrevF16", + 0x35 => "VMulF16", + 0x39 => "VMaxF16", + 0x3A => "VMinF16", _ => string.Empty, }; @@ -1086,6 +1092,14 @@ public static class Gen5ShaderTranslator 0xC5 => "VCmpNeU32", 0xC6 => "VCmpGeU32", 0xC7 => "VCmpTU32", + 0xC8 => "VCmpFF16", + 0xC9 => "VCmpLtF16", + 0xCA => "VCmpEqF16", + 0xCB => "VCmpLeF16", + 0xCC => "VCmpGtF16", + 0xCD => "VCmpLgF16", + 0xCE => "VCmpGeF16", + 0xCF => "VCmpOF16", 0xD0 => "VCmpxFU32", 0xD1 => "VCmpxLtU32", 0xD2 => "VCmpxEqU32", @@ -1094,6 +1108,30 @@ public static class Gen5ShaderTranslator 0xD5 => "VCmpxNeU32", 0xD6 => "VCmpxGeU32", 0xD7 => "VCmpxTU32", + 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", _ => string.Empty, }; diff --git a/tests/SharpEmu.ShaderCompiler.Metal.Tests/Gen5MslF16CompareTests.cs b/tests/SharpEmu.ShaderCompiler.Metal.Tests/Gen5MslF16CompareTests.cs new file mode 100644 index 00000000..c77dca5c --- /dev/null +++ b/tests/SharpEmu.ShaderCompiler.Metal.Tests/Gen5MslF16CompareTests.cs @@ -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 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); + } + } +} diff --git a/tests/SharpEmu.ShaderCompiler.Metal.Tests/MslFloat16ArithmeticTests.cs b/tests/SharpEmu.ShaderCompiler.Metal.Tests/MslFloat16ArithmeticTests.cs new file mode 100644 index 00000000..e55b1ca1 --- /dev/null +++ b/tests/SharpEmu.ShaderCompiler.Metal.Tests/MslFloat16ArithmeticTests.cs @@ -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", 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); + } +} diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Gen5Float16ArithmeticTests.cs b/tests/SharpEmu.ShaderCompiler.Tests/Gen5Float16ArithmeticTests.cs new file mode 100644 index 00000000..e4387053 --- /dev/null +++ b/tests/SharpEmu.ShaderCompiler.Tests/Gen5Float16ArithmeticTests.cs @@ -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 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 ReadOpcodes(byte[] spirv) => + ReadInstructions(spirv) + .Select(instruction => instruction.Opcode) + .ToArray(); + + private static IReadOnlyList 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 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 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; + } + } +} diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Gen5VopcF16Tests.cs b/tests/SharpEmu.ShaderCompiler.Tests/Gen5VopcF16Tests.cs new file mode 100644 index 00000000..d2d25bea --- /dev/null +++ b/tests/SharpEmu.ShaderCompiler.Tests/Gen5VopcF16Tests.cs @@ -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 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 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 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 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; + } + } +}