From 472fc96a37e4d39a98f59327357ca80455fdd548 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Victor=20Amorim?= Date: Mon, 20 Jul 2026 03:09:07 -0300 Subject: [PATCH] [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. --- .../Gen5SpirvTranslator.Alu.cs | 55 ++++++++++++++----- .../SharpEmu.Tools.GpuConformance/Program.cs | 6 ++ tools/SharpEmu.Tools.ShaderDump/Program.cs | 4 ++ 3 files changed, 50 insertions(+), 15 deletions(-) diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs index 1e5e1f38..7c6415bc 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs @@ -1015,12 +1015,6 @@ public static partial class Gen5SpirvTranslator return false; } - if (control.Clamp) - { - error = $"unsupported vop3p modifiers (clamp) for {instruction.Opcode}"; - return false; - } - var sourceCount = instruction.Opcode == "VPkFmaF16" ? 3 : 2; 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. + // 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( Gen5ShaderInstruction instruction, Gen5Vop3pControl control, @@ -1047,21 +1049,44 @@ public static partial class Gen5SpirvTranslator { var left = EmitPackedF16Operand(instruction, control, 0, highLane); var right = EmitPackedF16Operand(instruction, control, 1, highLane); + uint value; if (instruction.Opcode == "VPkFmaF16") { 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), - "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)); + value = EmitClampToUnitInterval(value); + } + + return EmitFloatToHalf(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 diff --git a/tools/SharpEmu.Tools.GpuConformance/Program.cs b/tools/SharpEmu.Tools.GpuConformance/Program.cs index 99b2e5e6..6c9ade06 100644 --- a/tools/SharpEmu.Tools.GpuConformance/Program.cs +++ b/tools/SharpEmu.Tools.GpuConformance/Program.cs @@ -17,6 +17,8 @@ // multiply-add would give 0x7A6A instead // [6] the same fma with the addend negated -> 0x7A6A packed (just below the // 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. // // 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. const uint ExpectedPkFma = 0x7A6B_7A6B; 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 { @@ -365,6 +370,7 @@ unsafe ("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 neg addend rounds down", words[6], ExpectedPkFmaNeg), + ("v_pk_fma_f16 clamp saturates to 1.0", words[7], ExpectedPkFmaClamp), }; var failures = 0; foreach (var (name, actual, expected) in results) diff --git a/tools/SharpEmu.Tools.ShaderDump/Program.cs b/tools/SharpEmu.Tools.ShaderDump/Program.cs index d76a4117..d2b78137 100644 --- a/tools/SharpEmu.Tools.ShaderDump/Program.cs +++ b/tools/SharpEmu.Tools.ShaderDump/Program.cs @@ -57,6 +57,8 @@ const ulong ProgramAddress = 0x100000; 0xCC114006, 0x18020300, // v_pk_min_f16 v6, 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 + 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 ]), ("mrt", true, [ @@ -158,8 +160,10 @@ const ulong ProgramAddress = 0x100000; 0x7E1202FF, 0x04EA04EA, // v_mov_b32 v9, 0x04EA04EA (~7.496e-5 packed) 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 + 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 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 ]), ];