mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-22 19:06:15 +08:00
[AGC] Decode VOP3P and emit packed f16 arithmetic (first slice) (#145)
* [AGC] Decode VOP3P and emit packed f16 arithmetic (first slice) On gfx10 the VOP3P family lives under its own 0b110011000 prefix (word0 top byte 0xCC), which the major-opcode switch currently routes to the SMEM branch, so packed instructions were decoded as scalar memory ops and emitted as silent no-ops. Intercept the exact 9-bit prefix ahead of the switch, decode the five packed-f16 arithmetic opcodes with their op_sel/op_sel_hi/neg_lo/neg_hi/clamp modifiers, and emit them as UnpackHalf2x16 -> component-wise f32 vec2 ops -> PackHalf2x16 so no Float16 capability is needed. Bit layout and opcode numbers pinned to LLVM MC test encodings (vop3p.s, gfx10_vop3p_literalv216.txt) and VOP3PInstructions.td. Unsupported modifiers, packed constants and out-of-scope packed opcodes fail with a clear error instead of emitting wrong results. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * [AGC] Make packed f16 exact and drop v_pk_fma_f16 (review response) Address the FP16 correctness review on the VOP3P slice. Replace GLSL UnpackHalf2x16/PackHalf2x16 with explicit integer f16<->f32 conversions (EmitHalfToFloat/EmitFloatToHalf): exact widening with subnormal normalisation, and narrowing with round-to-nearest-even, overflow-to-Inf and NaN/Inf handling. Their subnormal and rounding behaviour no longer depends on implementation-defined float-controls modes. With exact conversions, v_pk_add_f16 and v_pk_mul_f16 are bit-exact to a true f16 op (f32 result rounds losslessly to f16; a f16 product fits in f32). Emit v_pk_min_f16/v_pk_max_f16 as fminnum_like/fmaxnum_like (NaN operand returns the other; ordered numeric compare) instead of GLSL FMin/FMax. v_pk_fma_f16 now fails emission loudly: a fused f16 FMA rounds once, an f32 multiply-add then pack double-rounds (fma(0x4100,0x7522,0x04EA) is 0x7A6B fused vs 0x7A6A via f32). Exact fused emulation is a planned follow-up slice. ShaderDump gains an Expect model (Translates/DecodeFails/EmitFails) and packed regressions: arith, non-default modifiers, and loud-failure pins for the fma case above and for clamp. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: tensorcrush <tensorcrush@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Antigravity AI <antigravity@gemini.com>
This commit is contained in:
@@ -948,6 +948,27 @@ public static partial class Gen5SpirvTranslator
|
||||
vector);
|
||||
break;
|
||||
}
|
||||
|
||||
case "VPkAddF16":
|
||||
case "VPkMulF16":
|
||||
case "VPkMinF16":
|
||||
case "VPkMaxF16":
|
||||
if (!TryEmitPackedF16(instruction, out result, out error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
case "VPkFmaF16":
|
||||
// Deliberately loud: a fused f16 FMA rounds the product+add once,
|
||||
// whereas doing the multiply-add in f32 and rounding to f16 at the
|
||||
// end double-rounds. Concrete miss: fma(0x4100, 0x7522, 0x04EA) is
|
||||
// 0x7A6B fused but 0x7A6A via f32. Exact emulation (round-to-odd
|
||||
// f32 product then RNE pack) is a planned follow-up slice.
|
||||
error =
|
||||
$"unsupported vop3p opcode {instruction.Opcode} " +
|
||||
"(fused f16 FMA requires single-rounding; deferred to a later slice)";
|
||||
return false;
|
||||
default:
|
||||
error = $"unsupported vector opcode {instruction.Opcode}";
|
||||
return false;
|
||||
@@ -976,6 +997,213 @@ public static partial class Gen5SpirvTranslator
|
||||
return true;
|
||||
}
|
||||
|
||||
// Packed f16 (VOP3P) arithmetic. Each source register holds two f16 values,
|
||||
// one per result lane. Every f16<->f32 conversion is done with the explicit
|
||||
// integer sequences below (EmitHalfToFloat / EmitFloatToHalf) instead of
|
||||
// GLSL UnpackHalf2x16 / PackHalf2x16, whose subnormal and rounding behaviour
|
||||
// is implementation-defined without float-controls execution modes. The two
|
||||
// lanes are computed independently: each operand half is widened exactly to
|
||||
// f32, op_sel/op_sel_hi pick the source half and neg_lo/neg_hi negate it, the
|
||||
// op runs in f32, and the result is rounded back to f16 with round-to-nearest-
|
||||
// even. For add and mul this is bit-exact to a true f16 op (the f32 result
|
||||
// rounds losslessly to f16 by the double-rounding theorem; a f16 product even
|
||||
// fits in f32 exactly). min/max carry no rounding, so they are exact once the
|
||||
// conversions are. v_pk_fma_f16 is intentionally not routed here because a
|
||||
// fused f16 FMA cannot be reproduced by an f32 multiply-add plus a pack.
|
||||
private bool TryEmitPackedF16(
|
||||
Gen5ShaderInstruction instruction,
|
||||
out uint result,
|
||||
out string error)
|
||||
{
|
||||
result = 0;
|
||||
error = string.Empty;
|
||||
if (instruction.Control is not Gen5Vop3pControl control)
|
||||
{
|
||||
error = $"missing vop3p control for {instruction.Opcode}";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (control.Clamp)
|
||||
{
|
||||
error = $"unsupported vop3p modifiers (clamp) for {instruction.Opcode}";
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var index = 0; index < 2; index++)
|
||||
{
|
||||
var source = instruction.Sources[index];
|
||||
if (source.Kind is not (Gen5OperandKind.VectorRegister or Gen5OperandKind.ScalarRegister))
|
||||
{
|
||||
error =
|
||||
$"unsupported vop3p operand {source} for {instruction.Opcode} (first slice: registers only)";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var low = EmitPackedF16Lane(instruction, control, highLane: false);
|
||||
var high = EmitPackedF16Lane(instruction, control, highLane: true);
|
||||
result = BitwiseOr(low, ShiftLeftLogical(high, UInt(16)));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Computes one result lane (low or high) as a packed 16-bit f16 value.
|
||||
private uint EmitPackedF16Lane(
|
||||
Gen5ShaderInstruction instruction,
|
||||
Gen5Vop3pControl control,
|
||||
bool highLane)
|
||||
{
|
||||
var left = EmitPackedF16Operand(instruction, control, 0, highLane);
|
||||
var right = EmitPackedF16Operand(instruction, control, 1, highLane);
|
||||
var value = instruction.Opcode switch
|
||||
{
|
||||
"VPkAddF16" => _module.AddInstruction(SpirvOp.FAdd, _floatType, left, right),
|
||||
"VPkMulF16" => _module.AddInstruction(SpirvOp.FMul, _floatType, left, right),
|
||||
"VPkMinF16" => EmitPackedF16MinMax(left, right, isMax: false),
|
||||
"VPkMaxF16" => EmitPackedF16MinMax(left, right, isMax: true),
|
||||
_ => left,
|
||||
};
|
||||
return EmitFloatToHalf(Bitcast(_uintType, value));
|
||||
}
|
||||
|
||||
// Reads source `index`, selects the half feeding this lane (op_sel / op_sel_hi),
|
||||
// widens it exactly to f32 and applies the lane's negate modifier (neg_lo / neg_hi).
|
||||
private uint EmitPackedF16Operand(
|
||||
Gen5ShaderInstruction instruction,
|
||||
Gen5Vop3pControl control,
|
||||
int index,
|
||||
bool highLane)
|
||||
{
|
||||
var raw = GetRawSource(instruction, index);
|
||||
var selectMask = highLane ? control.OpSelHiMask : control.OpSelMask;
|
||||
var half = ((selectMask >> index) & 1) != 0
|
||||
? ShiftRightLogical(raw, UInt(16))
|
||||
: raw;
|
||||
var value = Bitcast(_floatType, EmitHalfToFloat(half));
|
||||
var negateMask = highLane ? control.NegHiMask : control.NegLoMask;
|
||||
if (((negateMask >> index) & 1) != 0)
|
||||
{
|
||||
value = _module.AddInstruction(SpirvOp.FNegate, _floatType, value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
// fminnum_like / fmaxnum_like: if one operand is NaN return the other; if both
|
||||
// are NaN return a NaN; otherwise the ordered smaller/larger. The ordering of
|
||||
// -0/+0 is unspecified under these opcodes, so the ordered compare is enough.
|
||||
private uint EmitPackedF16MinMax(uint left, uint right, bool isMax)
|
||||
{
|
||||
var compare = _module.AddInstruction(
|
||||
isMax ? SpirvOp.FOrdGreaterThan : SpirvOp.FOrdLessThan,
|
||||
_boolType,
|
||||
left,
|
||||
right);
|
||||
var numeric = _module.AddInstruction(
|
||||
SpirvOp.Select, _floatType, compare, left, right);
|
||||
var leftNan = _module.AddInstruction(SpirvOp.IsNan, _boolType, left);
|
||||
var rightNan = _module.AddInstruction(SpirvOp.IsNan, _boolType, right);
|
||||
var withRight = _module.AddInstruction(
|
||||
SpirvOp.Select, _floatType, rightNan, left, numeric);
|
||||
return _module.AddInstruction(
|
||||
SpirvOp.Select, _floatType, leftNan, right, withRight);
|
||||
}
|
||||
|
||||
// Widens an f16 value held in the low 16 bits of `halfBits` to an f32 bit
|
||||
// pattern, exactly (subnormals normalised, Inf/NaN and signed zero preserved).
|
||||
// Mirrors the branchless HalfToFloat reference validated against System.Half.
|
||||
private uint EmitHalfToFloat(uint halfBits)
|
||||
{
|
||||
var sign = ShiftLeftLogical(BitwiseAnd(halfBits, UInt(0x8000)), UInt(16));
|
||||
var exponent = BitwiseAnd(ShiftRightLogical(halfBits, UInt(10)), UInt(0x1F));
|
||||
var mantissa = BitwiseAnd(halfBits, UInt(0x3FF));
|
||||
|
||||
var normal = BitwiseOr(
|
||||
ShiftLeftLogical(IAdd(exponent, UInt(112)), UInt(23)),
|
||||
ShiftLeftLogical(mantissa, UInt(13)));
|
||||
var infinityNan = BitwiseOr(UInt(0x7F80_0000), ShiftLeftLogical(mantissa, UInt(13)));
|
||||
|
||||
// Subnormal: normalise the mantissa. FindUMsb of (mantissa | 1) keeps the
|
||||
// op defined when mantissa is 0; that lane is discarded by the select below.
|
||||
var highBit = Ext(75, _uintType, BitwiseOr(mantissa, UInt(1)));
|
||||
var shift = ISubU(UInt(23), highBit);
|
||||
var subFraction = BitwiseAnd(ShiftLeftLogical(mantissa, shift), UInt(0x7F_FFFF));
|
||||
var subnormal = SelectU(
|
||||
IsNotZero(mantissa),
|
||||
BitwiseOr(ShiftLeftLogical(IAdd(highBit, UInt(103)), UInt(23)), subFraction),
|
||||
UInt(0));
|
||||
|
||||
var magnitude = SelectU(
|
||||
Equal(exponent, 0),
|
||||
subnormal,
|
||||
SelectU(Equal(exponent, 31), infinityNan, normal));
|
||||
return BitwiseOr(sign, magnitude);
|
||||
}
|
||||
|
||||
// Narrows an f32 bit pattern to an f16 value in the low 16 bits, rounding to
|
||||
// nearest even (subnormals, overflow-to-Inf and NaN/Inf handled). Mirrors the
|
||||
// branchless FloatToHalf reference validated exhaustively against System.Half.
|
||||
private uint EmitFloatToHalf(uint bits)
|
||||
{
|
||||
var sign = BitwiseAnd(ShiftRightLogical(bits, UInt(16)), UInt(0x8000));
|
||||
var absolute = BitwiseAnd(bits, UInt(0x7FFF_FFFF));
|
||||
|
||||
var isInfinityNan = UCmp(SpirvOp.UGreaterThanEqual, absolute, UInt(0x7F80_0000));
|
||||
var isNan = UCmp(SpirvOp.UGreaterThan, absolute, UInt(0x7F80_0000));
|
||||
var infinityNan = BitwiseOr(
|
||||
BitwiseOr(sign, UInt(0x7C00)),
|
||||
SelectU(isNan, UInt(0x200), UInt(0)));
|
||||
|
||||
var exponent = ShiftRightLogical(absolute, UInt(23));
|
||||
var mantissa = BitwiseAnd(absolute, UInt(0x7F_FFFF));
|
||||
var significand = BitwiseOr(mantissa, UInt(0x80_0000));
|
||||
|
||||
// Normal path: round the 24-bit significand down to 11 bits (>> 13) with
|
||||
// round-to-nearest-even; the carry folds naturally into the exponent.
|
||||
var roundBit = BitwiseAnd(ShiftRightLogical(significand, UInt(13)), UInt(1));
|
||||
var rounded = ShiftRightLogical(IAdd(IAdd(significand, UInt(0xFFF)), roundBit), UInt(13));
|
||||
var halfExponent = ISubU(exponent, UInt(112));
|
||||
var normalBits = IAdd(ShiftLeftLogical(halfExponent, UInt(10)), ISubU(rounded, UInt(0x400)));
|
||||
var normal = SelectU(
|
||||
UCmp(SpirvOp.UGreaterThanEqual, exponent, UInt(113)),
|
||||
SelectU(UCmp(SpirvOp.UGreaterThanEqual, normalBits, UInt(0x7C00)), UInt(0x7C00), normalBits),
|
||||
UInt(0));
|
||||
|
||||
// Subnormal path: value = round(significand >> (126 - exponent)) with RNE.
|
||||
// The shift is clamped to 25 so it stays defined; on this path it is >= 14.
|
||||
var distance = ISubU(UInt(126), exponent);
|
||||
var shift = SelectU(UCmp(SpirvOp.UGreaterThan, distance, UInt(25)), UInt(25), distance);
|
||||
var shiftMask = ISubU(ShiftLeftLogical(UInt(1), shift), UInt(1));
|
||||
var halfWay = ShiftLeftLogical(UInt(1), ISubU(shift, UInt(1)));
|
||||
var lowBits = BitwiseAnd(significand, shiftMask);
|
||||
var quotient = ShiftRightLogical(significand, shift);
|
||||
var roundUp = _module.AddInstruction(
|
||||
SpirvOp.LogicalOr,
|
||||
_boolType,
|
||||
UCmp(SpirvOp.UGreaterThan, lowBits, halfWay),
|
||||
_module.AddInstruction(
|
||||
SpirvOp.LogicalAnd,
|
||||
_boolType,
|
||||
Equal(lowBits, halfWay),
|
||||
IsNotZero(BitwiseAnd(quotient, UInt(1)))));
|
||||
var subnormal = IAdd(quotient, SelectU(roundUp, UInt(1), UInt(0)));
|
||||
|
||||
var isSubnormal = UCmp(SpirvOp.ULessThanEqual, exponent, UInt(112));
|
||||
var finite = SelectU(isSubnormal, subnormal, normal);
|
||||
return SelectU(isInfinityNan, infinityNan, BitwiseOr(sign, finite));
|
||||
}
|
||||
|
||||
private uint SelectU(uint condition, uint whenTrue, uint whenFalse) =>
|
||||
_module.AddInstruction(SpirvOp.Select, _uintType, condition, whenTrue, whenFalse);
|
||||
|
||||
private uint UCmp(SpirvOp operation, uint left, uint right) =>
|
||||
_module.AddInstruction(operation, _boolType, left, right);
|
||||
|
||||
private uint Equal(uint value, uint constant) =>
|
||||
_module.AddInstruction(SpirvOp.IEqual, _boolType, value, UInt(constant));
|
||||
|
||||
private uint ISubU(uint left, uint right) =>
|
||||
_module.AddInstruction(SpirvOp.ISub, _uintType, left, right);
|
||||
|
||||
private bool TryEmitVectorCompare(
|
||||
Gen5ShaderInstruction instruction,
|
||||
out string error)
|
||||
|
||||
@@ -237,6 +237,17 @@ public sealed record Gen5SdwaControl(
|
||||
bool Clamp,
|
||||
uint? ScalarDestination) : Gen5InstructionControl;
|
||||
|
||||
// Packed (VOP3P) source and destination modifiers. Each mask holds one bit per
|
||||
// source operand. OpSel/OpSelHi pick which 16-bit half of a source feeds the low
|
||||
// and high result lanes respectively; NegLo/NegHi negate the value routed to each
|
||||
// lane. Clamp saturates each output half to [0, 1].
|
||||
public sealed record Gen5Vop3pControl(
|
||||
uint OpSelMask,
|
||||
uint OpSelHiMask,
|
||||
uint NegLoMask,
|
||||
uint NegHiMask,
|
||||
bool Clamp) : Gen5InstructionControl;
|
||||
|
||||
public sealed record Gen5DppControl(
|
||||
uint Control,
|
||||
bool FetchInactive,
|
||||
|
||||
@@ -562,6 +562,22 @@ public static class Gen5ShaderTranslator
|
||||
return DecodeSop(word, out name, out sizeDwords, out error);
|
||||
}
|
||||
|
||||
// gfx10 moved VOP3P (packed 16-bit math) to its own 0b110011000 prefix
|
||||
// (word0 top byte 0xCC), separate from the VOP3 block. Match the full
|
||||
// 9-bit prefix here, before the coarse major-opcode switch, so packed
|
||||
// instructions are not misread as one of the neighbouring encodings.
|
||||
if ((word & 0xFF800000u) == 0xCC000000u)
|
||||
{
|
||||
encoding = Gen5ShaderEncoding.Vop3p;
|
||||
if (!ctx.TryReadUInt32(baseAddress + pc + sizeof(uint), out var vop3pExtra))
|
||||
{
|
||||
error = $"vop3p-extra-read-failed pc=0x{pc:X}";
|
||||
return false;
|
||||
}
|
||||
|
||||
return DecodeVop3p(word, vop3pExtra, out name, out sizeDwords, out error);
|
||||
}
|
||||
|
||||
switch (word >> 26)
|
||||
{
|
||||
case 0x33:
|
||||
@@ -1160,6 +1176,37 @@ public static class Gen5ShaderTranslator
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool DecodeVop3p(
|
||||
uint word,
|
||||
uint extra,
|
||||
out string name,
|
||||
out uint sizeDwords,
|
||||
out string error)
|
||||
{
|
||||
var opcode = (word >> 16) & 0x7F;
|
||||
var src0 = extra & 0x1FF;
|
||||
var src1 = (extra >> 9) & 0x1FF;
|
||||
var src2 = (extra >> 18) & 0x1FF;
|
||||
sizeDwords = src0 == 0xFF || src1 == 0xFF || src2 == 0xFF ? 3u : 2u;
|
||||
error = string.Empty;
|
||||
|
||||
// Opcode numbers taken from LLVM's AMDGPU VOP3PInstructions.td and the
|
||||
// gfx9/gfx10 MC test encodings; they are unchanged across gfx9 and gfx10.
|
||||
// Unhandled packed opcodes (integer, fma_mix, ...) stay opaque here and
|
||||
// fail loudly at emission rather than being silently mis-emitted.
|
||||
name = opcode switch
|
||||
{
|
||||
0x0E => "VPkFmaF16",
|
||||
0x0F => "VPkAddF16",
|
||||
0x10 => "VPkMulF16",
|
||||
0x11 => "VPkMinF16",
|
||||
0x12 => "VPkMaxF16",
|
||||
_ => $"Vop3pRaw{opcode:X2}",
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool DecodeDs(
|
||||
uint word,
|
||||
out string name,
|
||||
@@ -1839,6 +1886,28 @@ public static class Gen5ShaderTranslator
|
||||
isVop3B ? (word >> 8) & 0x7F : null);
|
||||
break;
|
||||
}
|
||||
case Gen5ShaderEncoding.Vop3p:
|
||||
{
|
||||
var extra = words[1];
|
||||
sources =
|
||||
[
|
||||
Gen5Operand.Source(extra & 0x1FF, literal),
|
||||
Gen5Operand.Source((extra >> 9) & 0x1FF, literal),
|
||||
Gen5Operand.Source((extra >> 18) & 0x1FF, literal),
|
||||
];
|
||||
destinations = [Gen5Operand.Vector(word & 0xFF)];
|
||||
|
||||
// op_sel_hi is split across both dwords: bits [1:0] live in word1
|
||||
// [28:27], bit [2] in word0 [14].
|
||||
var opSelHi = ((extra >> 27) & 0x3) | (((word >> 14) & 0x1) << 2);
|
||||
control = new Gen5Vop3pControl(
|
||||
(word >> 11) & 0x7,
|
||||
opSelHi,
|
||||
(extra >> 29) & 0x7,
|
||||
(word >> 8) & 0x7,
|
||||
((word >> 15) & 1) != 0);
|
||||
break;
|
||||
}
|
||||
case Gen5ShaderEncoding.Ds:
|
||||
{
|
||||
var extra = words[1];
|
||||
|
||||
Reference in New Issue
Block a user