diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs index 5b28d7a4..e6dd722a 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs @@ -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) diff --git a/src/SharpEmu.ShaderCompiler/Gen5ShaderIr.cs b/src/SharpEmu.ShaderCompiler/Gen5ShaderIr.cs index 8d26be7a..d00a6e89 100644 --- a/src/SharpEmu.ShaderCompiler/Gen5ShaderIr.cs +++ b/src/SharpEmu.ShaderCompiler/Gen5ShaderIr.cs @@ -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, diff --git a/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs b/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs index 3bfa8cab..4b49a267 100644 --- a/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs +++ b/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs @@ -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];