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;
+ }
+ }
+}