[AGC] Support the clamp modifier on packed f16 VOP3P ops (#460)

The VOP3P emitter rejected any packed op with the clamp bit set. Clamp
saturates each f16 output half to [0, 1] (and flushes NaN to 0, matching
RDNA), so games that emit clamped packed arithmetic fell back to a loud
emit failure.

Apply the saturation to the f32 result of each lane, before it is
narrowed back to f16. Because 0.0 and 1.0 are exact in both f32 and f16
and the clamp is monotonic, clamping in f32 and then rounding to f16
yields the same value as clamping the f16 result directly; for the fused
multiply-add the pre-narrowing value is the round-to-odd f32, which
preserves that equivalence through the final round-to-nearest-even. The
saturation uses ordered compares so a NaN result collapses to 0 without a
separate IsNan test.

Verification:
- The local exact-reference harness now also clamps: add, mul, and fma
  each compared against an f16-domain clamp reference (NaN -> 0, else
  [0, 1]) over directed boundary inputs and 24M random cases. 0
  mismatches, alongside the existing 34M unclamped fma cases.
- ShaderDump pk-f16 gains a clamped add and a clamped fma; all decode and
  emit.
- The exec program computes the pinned fma with clamp (both lanes exceed
  1.0, so each saturates to 0x3C00) and stores it at offset 28;
  GpuConformance checks it on device. All values match on an AMD Radeon
  RX 7700 XT.
This commit is contained in:
João Victor Amorim
2026-07-20 03:09:07 -03:00
committed by GitHub
parent 33be88bdf9
commit 472fc96a37
3 changed files with 50 additions and 15 deletions
@@ -1015,12 +1015,6 @@ public static partial class Gen5SpirvTranslator
return false; return false;
} }
if (control.Clamp)
{
error = $"unsupported vop3p modifiers (clamp) for {instruction.Opcode}";
return false;
}
var sourceCount = instruction.Opcode == "VPkFmaF16" ? 3 : 2; var sourceCount = instruction.Opcode == "VPkFmaF16" ? 3 : 2;
for (var index = 0; index < sourceCount; index++) for (var index = 0; index < sourceCount; index++)
{ {
@@ -1040,6 +1034,14 @@ public static partial class Gen5SpirvTranslator
} }
// Computes one result lane (low or high) as a packed 16-bit f16 value. // Computes one result lane (low or high) as a packed 16-bit f16 value.
// The op runs in f32 and its result is narrowed back to f16 exactly (see
// EmitFloatToHalf). When the clamp modifier is set the pre-narrowing f32
// value is saturated to [0, 1] first; because 0.0 and 1.0 are exact in both
// f32 and f16 and the clamp is monotonic, clamping before the narrowing
// gives the same f16 the hardware produces by clamping the f16 result. For
// the fused multiply-add the pre-narrowing value is the round-to-odd f32
// from EmitPackedF16FusedMultiplyAdd, and round-to-odd preserves that
// equivalence through the final round-to-nearest-even.
private uint EmitPackedF16Lane( private uint EmitPackedF16Lane(
Gen5ShaderInstruction instruction, Gen5ShaderInstruction instruction,
Gen5Vop3pControl control, Gen5Vop3pControl control,
@@ -1047,21 +1049,44 @@ public static partial class Gen5SpirvTranslator
{ {
var left = EmitPackedF16Operand(instruction, control, 0, highLane); var left = EmitPackedF16Operand(instruction, control, 0, highLane);
var right = EmitPackedF16Operand(instruction, control, 1, highLane); var right = EmitPackedF16Operand(instruction, control, 1, highLane);
uint value;
if (instruction.Opcode == "VPkFmaF16") if (instruction.Opcode == "VPkFmaF16")
{ {
var addend = EmitPackedF16Operand(instruction, control, 2, highLane); var addend = EmitPackedF16Operand(instruction, control, 2, highLane);
return EmitFloatToHalf(EmitPackedF16FusedMultiplyAdd(left, right, addend)); value = EmitPackedF16FusedMultiplyAdd(left, right, addend);
}
else
{
value = Bitcast(_uintType, 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,
});
} }
var value = instruction.Opcode switch if (control.Clamp)
{ {
"VPkAddF16" => _module.AddInstruction(SpirvOp.FAdd, _floatType, left, right), value = EmitClampToUnitInterval(value);
"VPkMulF16" => _module.AddInstruction(SpirvOp.FMul, _floatType, left, right), }
"VPkMinF16" => EmitPackedF16MinMax(left, right, isMax: false),
"VPkMaxF16" => EmitPackedF16MinMax(left, right, isMax: true), return EmitFloatToHalf(value);
_ => left, }
};
return EmitFloatToHalf(Bitcast(_uintType, value)); // Saturates an f32 bit pattern to [0, 1] the way the VOP3P clamp modifier
// does: below 0 (and NaN, since the ordered compare is false for it) becomes
// 0, above 1 becomes 1. Ordered compares match the hardware's NaN-to-zero
// behaviour without a separate IsNan test.
private uint EmitClampToUnitInterval(uint valueBits)
{
var value = Bitcast(_floatType, valueBits);
var aboveZero = _module.AddInstruction(SpirvOp.FOrdGreaterThan, _boolType, value, Float(0));
var lowerBounded = _module.AddInstruction(SpirvOp.Select, _floatType, aboveZero, value, Float(0));
var belowOne = _module.AddInstruction(SpirvOp.FOrdLessThan, _boolType, lowerBounded, Float(1));
var clamped = _module.AddInstruction(SpirvOp.Select, _floatType, belowOne, lowerBounded, Float(1));
return Bitcast(_uintType, clamped);
} }
// Fused f16 multiply-add with a single rounding, emulated in f32 without the // Fused f16 multiply-add with a single rounding, emulated in f32 without the
@@ -17,6 +17,8 @@
// multiply-add would give 0x7A6A instead // multiply-add would give 0x7A6A instead
// [6] the same fma with the addend negated -> 0x7A6A packed (just below the // [6] the same fma with the addend negated -> 0x7A6A packed (just below the
// same midpoint), pinning the opposite rounding direction // same midpoint), pinning the opposite rounding direction
// [7] the pinned fma with the clamp modifier -> 0x3C00 packed (both lanes
// exceed 1.0, so each saturates to 1.0)
// Every other word of the buffer must still hold the sentinel afterwards. // Every other word of the buffer must still hold the sentinel afterwards.
// //
// Creating the compute pipeline doubles as a driver-acceptance check for the // Creating the compute pipeline doubles as a driver-acceptance check for the
@@ -45,6 +47,9 @@ var expectedRestored = BitConverter.SingleToUInt32Bits(1.5f);
// addend decides the rounding direction under a single fused rounding. // addend decides the rounding direction under a single fused rounding.
const uint ExpectedPkFma = 0x7A6B_7A6B; const uint ExpectedPkFma = 0x7A6B_7A6B;
const uint ExpectedPkFmaNeg = 0x7A6A_7A6A; const uint ExpectedPkFmaNeg = 0x7A6A_7A6A;
// Both lanes of the pinned fma are ~52560, far above 1.0, so the clamp modifier
// saturates each to 1.0 (0x3C00 in f16).
const uint ExpectedPkFmaClamp = 0x3C00_3C00;
unsafe unsafe
{ {
@@ -365,6 +370,7 @@ unsafe
("store after exec restore (offset 16)", words[4], expectedRestored), ("store after exec restore (offset 16)", words[4], expectedRestored),
("v_pk_fma_f16 fused rounds up at midpoint", words[5], ExpectedPkFma), ("v_pk_fma_f16 fused rounds up at midpoint", words[5], ExpectedPkFma),
("v_pk_fma_f16 neg addend rounds down", words[6], ExpectedPkFmaNeg), ("v_pk_fma_f16 neg addend rounds down", words[6], ExpectedPkFmaNeg),
("v_pk_fma_f16 clamp saturates to 1.0", words[7], ExpectedPkFmaClamp),
}; };
var failures = 0; var failures = 0;
foreach (var (name, actual, expected) in results) foreach (var (name, actual, expected) in results)
@@ -57,6 +57,8 @@ const ulong ProgramAddress = 0x100000;
0xCC114006, 0x18020300, // v_pk_min_f16 v6, v0, v1 0xCC114006, 0x18020300, // v_pk_min_f16 v6, v0, v1
0xCC124007, 0x18020300, // v_pk_max_f16 v7, v0, v1 0xCC124007, 0x18020300, // v_pk_max_f16 v7, v0, v1
0xCC0E4408, 0x9C0A0300, // v_pk_fma_f16 v8, v0, v1, neg_lo:[0,0,1] neg_hi:[0,0,1] v2 0xCC0E4408, 0x9C0A0300, // v_pk_fma_f16 v8, v0, v1, neg_lo:[0,0,1] neg_hi:[0,0,1] v2
0xCC0FC009, 0x18020000, // v_pk_add_f16 v9, v0, v0 clamp (2.5+2.5=5 -> saturates to 1.0)
0xCC0EC40A, 0x1C0A0300, // v_pk_fma_f16 v10, v0, v1, v2 clamp
0xBF810000, // s_endpgm 0xBF810000, // s_endpgm
]), ]),
("mrt", true, [ ("mrt", true, [
@@ -158,8 +160,10 @@ const ulong ProgramAddress = 0x100000;
0x7E1202FF, 0x04EA04EA, // v_mov_b32 v9, 0x04EA04EA (~7.496e-5 packed) 0x7E1202FF, 0x04EA04EA, // v_mov_b32 v9, 0x04EA04EA (~7.496e-5 packed)
0xCC0E400A, 0x1C261107, // v_pk_fma_f16 v10, v7, v8, v9 0xCC0E400A, 0x1C261107, // v_pk_fma_f16 v10, v7, v8, v9
0xCC0E440B, 0x9C261107, // v_pk_fma_f16 v11, v7, v8, neg_lo:[0,0,1] neg_hi:[0,0,1] v9 0xCC0E440B, 0x9C261107, // v_pk_fma_f16 v11, v7, v8, neg_lo:[0,0,1] neg_hi:[0,0,1] v9
0xCC0EC00C, 0x1C261107, // v_pk_fma_f16 v12, v7, v8, v9 clamp (>=1 -> saturates to 1.0)
0xE0700014, 0x80020A00, // buffer_store_dword v10, off, s[8:11], 0 offset:20 0xE0700014, 0x80020A00, // buffer_store_dword v10, off, s[8:11], 0 offset:20
0xE0700018, 0x80020B00, // buffer_store_dword v11, off, s[8:11], 0 offset:24 0xE0700018, 0x80020B00, // buffer_store_dword v11, off, s[8:11], 0 offset:24
0xE070001C, 0x80020C00, // buffer_store_dword v12, off, s[8:11], 0 offset:28
0xBF810000, // s_endpgm 0xBF810000, // s_endpgm
]), ]),
]; ];