shader: add compact f16 arithmetic and compare lowering (#840)

Co-authored-by: Foued Attar <attar.foued@gmail.com>
This commit is contained in:
frangametv
2026-08-24 13:59:51 +02:00
committed by GitHub
parent f8a826ec1b
commit 2b8ef7d8fa
7 changed files with 790 additions and 14 deletions
@@ -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;
}
/// <summary>Reads the selected 16-bit half as a widened float.</summary>
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<half>((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;
}
/// <summary>Rounds to f16 and preserves the unselected VGPR half.</summary>
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<ushort>(half({expression}))";
return ((control?.OperandSelect ?? 0) & 8) != 0
? $"((v[{destination}] & 0x0000FFFFu) | (({packed}) << 16))"
: $"((v[{destination}] & 0xFFFF0000u) | ({packed}))";
}
/// <summary>
/// Wraps a float expression with VOP3/SDWA output modifiers and clamp,
/// then bitcasts back to the register file's uint domain.
@@ -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)
@@ -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,
};