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 ]), ];