diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MixPrecision.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MixPrecision.cpp index d8c1506e3..f7d68a606 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/MixPrecision.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MixPrecision.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -115,6 +116,30 @@ constexpr std::array RoundingRows{{ {0x00003c01u, 0x00004200u, 0x00800000u, 3u, 0x4202u}, }}; +struct DirectedRow { + std::uint32_t a, b, c; + std::array expected; +}; + +constexpr std::array DirectedRows{{ + {0xbfc00000u, 0x33800000u, 0x00800000u, {0x8001u, 0x8001u, 0x8002u, 0x8001u}}, + {0x3fc00000u, 0x33800000u, 0x00800000u, {0x0002u, 0x0002u, 0x0001u, 0x0001u}}, + {0x3f800000u, 0x3f801400u, 0x00000000u, {0x3c01u, 0x3c01u, 0x3c00u, 0x3c00u}}, + {0xbf800000u, 0x3f801400u, 0x00000000u, {0xbc01u, 0xbc00u, 0xbc01u, 0xbc00u}}, + {0x477ff000u, 0x3f800000u, 0x00000000u, {0x7c00u, 0x7c00u, 0x7bffu, 0x7bffu}}, + {0xc77ff000u, 0x3f800000u, 0x00000000u, {0xfc00u, 0xfbffu, 0xfc00u, 0xfbffu}}, + {0x49742400u, 0x3f800000u, 0x00000000u, {0x7c00u, 0x7c00u, 0x7bffu, 0x7bffu}}, + {0x3fc00000u, 0x40000000u, 0x3e800000u, {0x4280u, 0x4280u, 0x4280u, 0x4280u}}, + {0x3f800000u, 0x3f800000u, 0xbf7fffffu, {0x0001u, 0x0001u, 0x0001u, 0x0001u}}, + {0xbf800000u, 0x3f800000u, 0x3f7fffffu, {0x8001u, 0x8001u, 0x8001u, 0x8001u}}, + {0x3f800000u, 0x3f800000u, 0xbf800000u, {0x0000u, 0x0000u, 0x8000u, 0x0000u}}, + {0x40000000u, 0x3f800000u, 0xc0000000u, {0x0000u, 0x0000u, 0x8000u, 0x0000u}}, + {0x00000000u, 0x3f800000u, 0x80000000u, {0x0000u, 0x0000u, 0x8000u, 0x0000u}}, + {0x80000000u, 0x3f800000u, 0x80000000u, {0x8000u, 0x8000u, 0x8000u, 0x8000u}}, + {0x33000000u, 0x3f000000u, 0x00000000u, {0x0000u, 0x0001u, 0x0000u, 0x0000u}}, + {0xb3000000u, 0x3f000000u, 0x00000000u, {0x8000u, 0x8000u, 0x8001u, 0x8000u}}, +}}; + float Quarter(std::uint32_t index) { return static_cast(static_cast(index % 17u) - 8) / 4.0f; } @@ -176,7 +201,7 @@ void FillRoundingInput() { } } -void Run(AgcDriver::VulkanDevice& device, std::span code) { +void Run(AgcDriver::VulkanDevice& device, std::span code, const std::optional& floatMode = std::nullopt) { Output.fill(0xdeadbeefu); std::vector userData(8, 0u); const auto input = BufferDescriptor(Input.data(), static_cast(Input.size())); @@ -192,6 +217,7 @@ void Run(AgcDriver::VulkanDevice& device, std::span code) { {0, 0, 0, 128} }; request.useCache = false; + request.context.floatMode = floatMode; const auto result = ShaderRecompiler::Recompile(request); device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); device.WaitIdle(); @@ -235,6 +261,25 @@ void CheckRounding() { } } +void FillDirectedInput() { + Input.fill(0u); + for (std::uint32_t tid = 0; tid < DirectedRows.size(); ++tid) { + const auto& row = DirectedRows[tid]; + std::copy_n(std::array{row.a, row.b, row.c}.begin(), 3, Input.begin() + tid * Inputs); + } +} + +void CheckDirected(std::uint32_t rounding) { + for (std::uint32_t tid = 0; tid < DirectedRows.size(); ++tid) { + const auto& row = DirectedRows[tid]; + const auto* out = &Output[tid * Results]; + const std::uint32_t expected = row.expected[rounding]; + const auto where = "mix f16 rounding " + std::to_string(rounding) + ": thread " + std::to_string(tid) + " (" + Hex(row.a) + ", " + Hex(row.b) + ", " + Hex(row.c) + ")"; + Require(out[0] == (0xabcd0000u | expected), where + " v_fma_mixlo_f16 is " + Hex(out[0]) + ", expected " + Hex(0xabcd0000u | expected)); + Require(out[1] == ((expected << 16u) | 0xabcdu), where + " v_fma_mixhi_f16 is " + Hex(out[1]) + ", expected " + Hex((expected << 16u) | 0xabcdu)); + } +} + } int main() { @@ -252,6 +297,11 @@ int main() { FillRoundingInput(); Run(*device, RoundingCode); CheckRounding(); + for (std::uint32_t rounding = 0; rounding < 4u; ++rounding) { + FillDirectedInput(); + Run(*device, RoundingCode, ShaderRecompiler::ShaderFloatMode{0xc0u | (rounding << 2u), true, false, false}); + CheckDirected(rounding); + } std::puts("mix precision tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/tests/VectorFusedMultiplyAdd.cpp b/core/libs/tests/VectorFusedMultiplyAdd.cpp index f17e7f083..3f2926517 100644 --- a/core/libs/tests/VectorFusedMultiplyAdd.cpp +++ b/core/libs/tests/VectorFusedMultiplyAdd.cpp @@ -105,7 +105,10 @@ void CheckMixHalf(const std::string& name, const std::array& w std::uint32_t exact = 0u; for (const auto* value : block.Instructions()) { Require(value->Opcode() != IrOpcode::FPFma32 && value->Opcode() != IrOpcode::FPMad32, name + " rounds to f32 before rounding to f16"); - if (value->Opcode() == IrOpcode::FPInterpolateF16) ++exact; + if (value->Opcode() != IrOpcode::FPInterpolateF16) continue; + ++exact; + const std::uint32_t rounding = (value->Argument(3)->ImmediateU32() >> InterpolationRound16Shift) & 3u; + Require(rounding == ((floatMode >> 2u) & 3u), name + " rounds in f16 mode " + std::to_string(rounding)); } Require(exact == 1u, name + " emits " + std::to_string(exact) + " product-sums rounded once to f16"); } @@ -146,7 +149,8 @@ int main() { CheckMixHalf("v_fma_mixlo_f16 op_sel_hi:[1,1,1]", {0xcc214005u, 0x1c220f06u}, RdnaOpcode::VMadMixloF16, 0xc0u); CheckMixHalf("v_fma_mixlo_f16 with f32 round toward zero", {0xcc210005u, 0x04220f06u}, RdnaOpcode::VMadMixloF16, 0xf3u); RejectMixHalf("v_fma_mixlo_f16 with f16 denormals flushed", {0xcc210005u, 0x04220f06u}, 0x30u); - RejectMixHalf("v_fma_mixhi_f16 with f16 round toward -inf", {0xcc220005u, 0x04220f06u}, 0xf8u); + CheckMixHalf("v_fma_mixhi_f16 with f16 round toward -inf", {0xcc220005u, 0x04220f06u}, RdnaOpcode::VMadMixhiF16, 0xf8u); + CheckMixHalf("v_fma_mixlo_f16 with f16 round toward zero", {0xcc210005u, 0x04220f06u}, RdnaOpcode::VMadMixloF16, 0xccu); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index deca904d9..85f9689e8 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -176,6 +176,7 @@ struct ShaderFloatMode { inline constexpr std::uint32_t InterpolationQuiet = 1u; inline constexpr std::uint32_t InterpolationFlush32 = 2u; inline constexpr std::uint32_t InterpolationFlush16 = 4u; +inline constexpr std::uint32_t InterpolationRound16Shift = 3u; struct GuestContext { std::uint32_t waveSize; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp index b9a4a629f..241522188 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp @@ -223,6 +223,23 @@ std::uint32_t RoundToPrecision(SpirvEmitterState& state, std::uint32_t value, st return ToF64(state, Join(state, {rounded.low, Binary(state, spv::OpBitwiseOr, u32, Binary(state, spv::OpBitwiseAnd, u32, rounded.high, ConstantU32(state, 0x7fffffffu)), sign)})); } +std::uint32_t RoundToPrecisionDirected(SpirvEmitterState& state, std::uint32_t value, std::uint32_t fractionBits, std::uint32_t smallestStep, std::uint32_t rounding) { + const auto bits = Split(state, FromF64(state, value)); + const auto exponent = Unary(state, spv::OpBitcast, TypeI32(state), ExponentField(state, bits.high)); + const auto step = Glsl(state, TypeI32(state), GLSLstd450SMax, Binary(state, spv::OpISub, TypeI32(state), exponent, ConstantI32(state, static_cast(fractionBits))), ConstantI32(state, static_cast(smallestStep))); + const auto unbiased = Binary(state, spv::OpISub, TypeI32(state), step, ConstantI32(state, 1023)); + const auto scaled = Exact(state, spv::OpFMul, value, PowerOfTwo(state, Unary(state, spv::OpSNegate, TypeI32(state), unbiased))); + std::uint32_t integral = 0; + if (rounding == 3u) { + integral = Trunc(state, FromF64(state, scaled)); + } else if (rounding == 2u) { + integral = Floor(state, FromF64(state, scaled)); + } else { + integral = Unary(state, spv::OpFNegate, TypeF64(state), Floor(state, FromF64(state, Unary(state, spv::OpFNegate, TypeF64(state), scaled)))); + } + return Exact(state, spv::OpFMul, integral, PowerOfTwo(state, unbiased)); +} + std::uint32_t F32BitsOfRounded(SpirvEmitterState& state, std::uint32_t value) { const auto u32 = TypeU32(state); const auto bits = Split(state, FromF64(state, value)); @@ -265,12 +282,32 @@ std::uint32_t Interpolate(SpirvEmitterState& state, std::uint32_t delta, std::ui base = halfResult ? FlushDenormal32(state, base, flush32, 0x00800000u) : FlushDenormal32(state, base, flush16, 0x38800000u); const auto product = Exact(state, spv::OpFMul, FromF32Exact(state, delta), FromF32Exact(state, coordinate)); const auto sum = AddRoundToOdd(state, product, FromF32Exact(state, base)); - auto rounded = halfResult ? RoundToPrecision(state, sum, 10u, 999u) : RoundToPrecision(state, sum, 23u, 874u); + const std::uint32_t rounding16 = halfResult ? (mode >> InterpolationRound16Shift) & 3u : 0u; + auto rounded = halfResult ? (rounding16 == 0u ? RoundToPrecision(state, sum, 10u, 999u) : RoundToPrecisionDirected(state, sum, 10u, 999u, rounding16)) : RoundToPrecision(state, sum, 23u, 874u); if (halfResult) { const auto bits = Split(state, FromF64(state, rounded)); const auto sign = Binary(state, spv::OpBitwiseAnd, u32, bits.high, ConstantU32(state, 0x80000000u)); - const auto overflow = Binary(state, spv::OpUGreaterThan, TypeBool(state), Binary(state, spv::OpBitwiseAnd, u32, bits.high, ConstantU32(state, 0x7fffffffu)), ConstantU32(state, 0x40f00000u)); - rounded = ToF64(state, Join(state, SelectBits(state, overflow, {ConstantU32(state, 0u), Binary(state, spv::OpBitwiseOr, u32, sign, ConstantU32(state, 0x40f00000u))}, bits))); + const auto magnitude = Binary(state, spv::OpBitwiseAnd, u32, bits.high, ConstantU32(state, 0x7fffffffu)); + if (rounding16 == 0u) { + const auto overflow = Binary(state, spv::OpUGreaterThan, TypeBool(state), magnitude, ConstantU32(state, 0x40f00000u)); + rounded = ToF64(state, Join(state, SelectBits(state, overflow, {ConstantU32(state, 0u), Binary(state, spv::OpBitwiseOr, u32, sign, ConstantU32(state, 0x40f00000u))}, bits))); + } else { + const auto overflow = Binary(state, spv::OpUGreaterThan, TypeBool(state), magnitude, ConstantU32(state, 0x40effc00u)); + const auto negative = Binary(state, spv::OpINotEqual, TypeBool(state), sign, ConstantU32(state, 0u)); + auto limit = ConstantU32(state, 0x40effc00u); + if (rounding16 == 1u) limit = Select(state, u32, negative, ConstantU32(state, 0x40effc00u), ConstantU32(state, 0x40f00000u)); + if (rounding16 == 2u) limit = Select(state, u32, negative, ConstantU32(state, 0x40f00000u), ConstantU32(state, 0x40effc00u)); + rounded = ToF64(state, Join(state, SelectBits(state, overflow, {ConstantU32(state, 0u), Binary(state, spv::OpBitwiseOr, u32, sign, limit)}, bits))); + if (rounding16 == 2u) { + const auto boolean = TypeBool(state); + const auto zeroBits = [&](F64Bits value, std::uint32_t highMask) { + return Binary(state, spv::OpLogicalAnd, boolean, Binary(state, spv::OpIEqual, boolean, Binary(state, spv::OpBitwiseAnd, u32, value.high, ConstantU32(state, highMask)), ConstantU32(state, 0u)), Binary(state, spv::OpIEqual, boolean, value.low, ConstantU32(state, 0u))); + }; + const auto positiveZeros = Binary(state, spv::OpLogicalAnd, boolean, zeroBits(Split(state, FromF64(state, product)), 0xffffffffu), zeroBits(Split(state, FromF64(state, FromF32Exact(state, base))), 0xffffffffu)); + const auto negativeZero = Binary(state, spv::OpLogicalAnd, boolean, zeroBits(Split(state, FromF64(state, sum)), 0x7fffffffu), Unary(state, spv::OpLogicalNot, boolean, positiveZeros)); + rounded = ToF64(state, Join(state, SelectBits(state, negativeZero, {ConstantU32(state, 0u), ConstantU32(state, 0x80000000u)}, Split(state, FromF64(state, rounded))))); + } + } } const auto finite = F32BitsOfRounded(state, rounded); const auto inputs = std::array{Unary(state, spv::OpBitcast, u32, delta), Unary(state, spv::OpBitcast, u32, coordinate), Unary(state, spv::OpBitcast, u32, base)}; diff --git a/core/shader/recompiler/Translation/src/FloatInstructions.cpp b/core/shader/recompiler/Translation/src/FloatInstructions.cpp index 2ca7925c1..b1ef1e9e2 100644 --- a/core/shader/recompiler/Translation/src/FloatInstructions.cpp +++ b/core/shader/recompiler/Translation/src/FloatInstructions.cpp @@ -230,8 +230,8 @@ bool TranslationContext::float16Binary(const RdnaInstruction& inst, IrOpcode opc } bool TranslationContext::float16Ternary(const RdnaInstruction& inst, IrOpcode opcode, bool accumulator, bool mix) { - if (mix && floatMode.has_value() && (floatMode->floatMode & 0xccu) != 0xc0u) { - throw std::runtime_error("v_fma_mixlo/mixhi_f16 at pc " + std::to_string(inst.programCounter) + " in FLOAT_MODE " + std::to_string(floatMode->floatMode) + " (f16 denormals not kept or rounding not to nearest even) is not implemented"); + if (mix && floatMode.has_value() && (floatMode->floatMode & 0xc0u) != 0xc0u) { + throw std::runtime_error("v_fma_mixlo/mixhi_f16 at pc " + std::to_string(inst.programCounter) + " in FLOAT_MODE " + std::to_string(floatMode->floatMode) + " (f16 denormals not kept) is not implemented"); } std::array args{}; for (std::uint32_t index = 0u; index < args.size(); ++index) { @@ -243,7 +243,8 @@ bool TranslationContext::float16Ternary(const RdnaInstruction& inst, IrOpcode op return true; } if (mix) { - writeF16(inst.destination, IrF32(ir.Emit(IrOpcode::FPInterpolateF16, IrType::F32, {args[0], args[1], args[2], &ir.Constant(0u)})), {args[0], args[1], args[2]}); + const std::uint32_t rounding = floatMode.has_value() ? ((floatMode->floatMode >> 2u) & 3u) << InterpolationRound16Shift : 0u; + writeF16(inst.destination, IrF32(ir.Emit(IrOpcode::FPInterpolateF16, IrType::F32, {args[0], args[1], args[2], &ir.Constant(rounding)})), {args[0], args[1], args[2]}); return true; } writeF16(inst.destination, IrF32(ir.Emit(opcode, IrType::F32, {args[0], args[1], args[2]})), {args[0], args[1], args[2]}); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index c6286c5c2..066712536 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -483,7 +483,7 @@ Throughout the project, every function at every stage either **does exactly what - The [dispatches that follow a compute queue's wait](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp) (libSceAgcDriver), up to the next wait that memory does not satisfy, a packet that writes guest memory other than a label, or a conditional packet, are resolved (memory captured, shader recompiled) when the wait passes, as the CP reads them right after it; the graphics queue is exempt (`APS5_GROUP_CAPTURE_ALL_QUEUES=1` includes it). The device still reads guest memory when the dispatch is issued; when that memory no longer matches the capture, the dispatch is resolved again from current memory. - [v_mullit_f32](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the hardware measured with the MODE register at IEEE=1 and denormals kept (a signaling NaN src0 is quieted only with IEEE=1, as measured at IEEE=0 too; a positive denormal src2 counts as positive); with f32 denormals flushed it follows the f32 denormals entry below, and denormal modes 1 and 2 throw. - [v_mad_legacy_f32 and v_mac_legacy_f32](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (VOP3 0x140, VOP2 0x06) round the product to f32 before the add, give a +0 product for a zero factor, and flush their f32 denormal sources, their product and their result to a zero of the same sign in every f32 denormal mode, an exception to the `FLOAT_MODE` model in [f32DenormalFlushFor](../../core/shader/recompiler/Translation/src/VectorInstructions.cpp). Measured on gfx1010 (RX 5700 XT) with `tools/hw-oracle/template.s`, assembled by the AMD Windows driver's `amd_comgr_2.dll` and launched through its `amdhip64_6.dll` instead of `oracle.c` and ROCr, over the 32 rows of the five `FmaLegacy.cpp` and the four `MacLegacy.cpp` encodings: identical in wave32 and wave64 at IEEE 0 and 1 with f32 denormal modes 0 to 3, except that the two signaling NaN rows are quieted only with IEEE=1 (#1281, #1290). A sum of two zeros takes its sign on the bits (+0 unless both are -0), as lavapipe returned -0 for +0 + -0. The PS5's gfx1013 is not measured; LLVM decodes the encoding as `v_mad_legacy_f32` there, as on gfx1010. RDNA2 differs: on gfx1031 (RX 6700 XT, measured by pandather in #1281) the encoding is `v_fma_legacy_f32` and fused, with the same zero rule and denormal-source flush, so an RDNA2 measurement is not evidence for the console. -- [v_fma_mixlo_f16 and v_fma_mixhi_f16](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) round the exact product-sum once to f16 with the f64 arithmetic of v_interp_p2_f16, so they need the Float64 capability. On RDNA2 (gfx1036) they follow the `FLOAT_MODE` f16 round and denormal fields, not the f32 round field: -1.5 * 2^-24 + 2^-126 from f32 sources (0xbfc00000, 0x33800000, 0x00800000) gives 0x8001 with f16 denormals kept and round to nearest even, 0x8000 with f16 denormals flushed and 0x8002 with f16 round toward -infinity. Only f16 denormals kept with round to nearest even is translated; another f16 mode throws, as for packed f16. `FP16_OVFL` set is not measured. +- [v_fma_mixlo_f16 and v_fma_mixhi_f16](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) round the exact product-sum once to f16 with the f64 arithmetic of v_interp_p2_f16, so they need the Float64 capability. On RDNA2 (gfx1036) they follow the `FLOAT_MODE` f16 round and denormal fields, not the f32 round field: -1.5 * 2^-24 + 2^-126 from f32 sources (0xbfc00000, 0x33800000, 0x00800000) gives 0x8001 with f16 denormals kept and round to nearest even, 0x8000 with f16 denormals flushed and 0x8002 with f16 round toward -infinity. With f16 denormals kept, every f16 rounding mode is translated: the exact product-sum is rounded once in the mode's direction. All four modes match RDNA2 (gfx1036) on the 2,233 rows of the #2631 sweep, including `FLOAT_MODE` 0xcc (toward zero, used by ps5-opengl) and the exact cancellations toward -infinity; the PS5's gfx1013 is not measured. A result beyond 65504 becomes infinity or 65504 as IEEE 754 gives for the mode. Flushed f16 denormals still throw, and packed f16 still throws in every mode but round to nearest even with f16 denormals kept. `FP16_OVFL` set is not measured. - [Signaling NaN inputs](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) of the f32 and f16 unary instructions (`v_sqrt/rcp/rsq/log/exp/sin/cos/fract/floor/ceil/trunc/rndne/rcp_iflag/frexp_mant`), `v_ldexp_f32/f16`, `v_frexp_mant_f16`, `v_div_fixup_f32/f16` and `v_mullit_f32` are returned quieted only with IEEE=1 and unchanged with IEEE=0, measured on RDNA2 (gfx1035) in both modes, and so are those of the f64 forms (`v_sqrt/rcp/rsq/fract/floor/ceil/trunc/rndne/frexp_mant/ldexp/div_fixup_f64`) and `v_cvt_f64_f32`, measured on RDNA2 (gfx1036) in both modes; shaders without a known float mode follow IEEE=0. `v_pack_b32_f16` quiets each signaling NaN half by the same rule, measured on RDNA2 (gfx1036) in both modes. A signaling NaN result of `v_cubesc/cubetc/cubema_f32` is quieted by the same rule and keeps the sign the instruction gives it, measured on RDNA2 (gfx1036) in both modes. `v_add/sub/subrev/mul/fma/fmac/fmamk/fmaak/mul_legacy_f32` and `v_fma_mix_f32` with f32 sources choose their NaN result on the bits, measured over 1000 operand triples in both modes: the first NaN source in operand order after its modifiers (the subtrahend negated, as the hardware adds its negation), quieted only with IEEE=1, or the default NaN 0xffc00000 for an invalid operation; in the fma forms an invalid product (inf * 0) gives the default NaN even when src2 is a NaN, and in the legacy forms a zero factor makes the product 0 even when the other factor is a NaN (a NaN src2 still wins). `v_mad_legacy_f32` and `v_mac_legacy_f32` follow the same rule with their separately rounded product, as measured on gfx1010 (#1281, #1290): a NaN source quieted only with IEEE=1, inf - inf giving 0xffc00000, the zero rule hiding a NaN factor and a NaN src2 winning. The f16 and f64 arithmetic, `v_fma_mix_f32` with f16 sources and the separately rounded mad/mac forms (not measurable on RDNA2) follow the same rule on hardware but their NaN bits are left to the driver. `v_cvt_f16_f32`, `v_cvt_f32_f64`, `v_dot2_f32_f16`, `v_fma_mixlo_f16` and `v_fma_mixhi_f16` always quiet and `v_div_scale_f32` never does: the f32 signaling NaN 0x7f812345 as src0 or src2 gives 0x7e09 from mixlo and mixhi at IEEE=0 and IEEE=1 on RDNA2 (gfx1036), where `v_fma_mix_f32` gives 0x7f812345 at IEEE=0 and 0x7fc12345 at IEEE=1. The payload of an f16 NaN source of mixlo and mixhi is left to the driver's f16 to f32 conversion, which NVIDIA replaces. - [Scalar f16 add, sub, subrev and mul denormals](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follow the `FLOAT_MODE` f16 denormal field at nearest-even: bit 0 preserves inputs and bit 1 preserves the result; otherwise a denormal is flushed to its signed zero. Measured on RDNA2 (gfx1036) with IEEE=0/1 and FP16_OVFL=0/1, over 32 operand pairs in all four denormal modes, including signed zeros, denormals, minimum normals, infinities and NaNs. Multiply detects tininess after rounding to 11 significant bits with an unbounded exponent, before gradual half packing: `0x3bff * 0x0400` flushes, while `0x3c01 * 0x03ff` rounds to `0x0400` and is kept. Directed rounding and ternary arithmetic keep their previous denormal lowering. Shaders without a known float mode keep the previous lowering. Scalar `v_sub_f16`/`v_subrev_f16` share `float16Binary` with add/mul and were measured on gfx1036 over the same operand set as add (1156 non-NaN-input rows per mode); NaN result bits still depend on the driver as recorded above. - [Packed f16 NaN results](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (`v_pk_add/mul/fma/min/max_f16`) follow the hardware measured on RDNA2 (gfx1035) at IEEE=0 and IEEE=1 with f16 round-to-nearest-even and f16 denormals kept: with IEEE=1 a signaling NaN input is quieted and min/max return the other operand only for a quiet NaN; with IEEE=0 NaN inputs pass through unquieted and min/max ignore signaling. In `v_pk_fma_f16` an invalid product (inf * 0) gives the default NaN even when src2 is a NaN. A shader without a known float mode is translated as IEEE=0, the mode titles use; a mode with another f16 rounding or that flushes f16 denormals throws.