From 1e4ac1371d84d234610a33373fdca9c9ff8c1015 Mon Sep 17 00:00:00 2001 From: nmzik Date: Thu, 22 Oct 2026 00:00:04 +0200 Subject: [PATCH] shader: return the first source when both float min/max inputs are NaN --- .../backend/spirv/spirvEmitterAluHelpers.cpp | 13 +++++++------ tests/ShaderRecompilerComputeTests.cpp | 10 ++++++---- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterAluHelpers.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterAluHelpers.cpp index 1f91231a8..c8a5e9b0f 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterAluHelpers.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterAluHelpers.cpp @@ -95,8 +95,8 @@ uint32_t EmitClassMaskF16(EmitterState& state, uint32_t bits, uint32_t mask) { static uint32_t EmitMinMaxF32(EmitterState& state, uint32_t lhs, uint32_t rhs, uint32_t lhs_bits, uint32_t rhs_bits, bool max_value) { - // RDNA2 V_MIN/V_MAX_F32 return the other source for a NaN source (S1 when both are NaN) - // and order -0 below +0. An unordered compare keeps S0 for a NaN S1 and, unlike NMin/NMax, + // PS5 V_MIN/V_MAX_F32 return the other source for a NaN source (S0 when both are NaN) + // and order -0 below +0. An ordered compare takes S1 for a NaN S0 and, unlike NMin/NMax, // always returns the exact bits of one source. const auto either = EmitOrU32(state, lhs_bits, rhs_bits); const auto both_zero = EmitCompareU32Constant(state, spv::OpIEqual, @@ -104,11 +104,11 @@ static uint32_t EmitMinMaxF32(EmitterState& state, uint32_t lhs, uint32_t rhs, u const auto zero = EmitBitcastU32ToF32(state, max_value ? EmitAndU32(state, lhs_bits, rhs_bits) : either); const auto keep_lhs = - Binary(state, max_value ? spv::OpFUnordGreaterThanEqual : spv::OpFUnordLessThan, + Binary(state, max_value ? spv::OpFOrdGreaterThanEqual : spv::OpFOrdLessThan, TypeBool(state), lhs, rhs); const auto ordered = Select(state, TypeF32(state), keep_lhs, lhs, rhs); const auto numeric = Select(state, TypeF32(state), both_zero, zero, ordered); - return Select(state, TypeF32(state), Unary(state, spv::OpIsNan, TypeBool(state), lhs), rhs, + return Select(state, TypeF32(state), Unary(state, spv::OpIsNan, TypeBool(state), rhs), lhs, numeric); } @@ -127,9 +127,10 @@ uint32_t EmitFPMedTri32(EmitterState& state, uint32_t a, uint32_t b, uint32_t c) const auto a_bits = EmitBitcastF32ToU32(state, a); const auto b_bits = EmitBitcastF32ToU32(state, b); const auto c_bits = EmitBitcastF32ToU32(state, c); - const auto min_ab = EmitMinMaxF32(state, a, b, a_bits, b_bits, false); + // The NaN path is RDNA2 MIN3, which keeps the later source when two are NaN. + const auto min_ba = EmitMinMaxF32(state, b, a, b_bits, a_bits, false); const auto min3 = - EmitMinMaxF32(state, min_ab, c, EmitBitcastF32ToU32(state, min_ab), c_bits, false); + EmitMinMaxF32(state, c, min_ba, c_bits, EmitBitcastF32ToU32(state, min_ba), false); const auto max_ab = EmitMinMaxF32(state, a, b, a_bits, b_bits, true); const auto max3 = EmitMinMaxF32(state, max_ab, c, EmitBitcastF32ToU32(state, max_ab), c_bits, true); diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index b87cc47e6..0055aed7d 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -27897,7 +27897,8 @@ TestCase VectorMinMaxF32NanAndSignedZeroEdges() { } // MIN/MAX/MIN3/MAX3 on runtime inputs, so no compiler can fold them. The expected bits follow -// the emulated rules: a NaN operand yields the other operand and signed zeros are ordered. +// the emulated rules: a NaN operand yields the other (S0 for two NaNs) and signed zeros are +// ordered. // Denormal operands are left out; their expected results are not pinned down here. TestCase VectorMinMaxF32RuntimeEdges(u32 wave) { using O = ShaderOpcode; @@ -27905,14 +27906,14 @@ TestCase VectorMinMaxF32RuntimeEdges(u32 wave) { const auto zero = [](u32 v) { return (v & 0x7fffffffu) == 0u; }; const auto value = [](u32 v) { return std::bit_cast(v); }; const auto min2 = [&](u32 a, u32 b) { - if (nan(a)) return b; if (nan(b)) return a; + if (nan(a)) return b; if (zero(a) && zero(b)) return a | b; return value(a) < value(b) ? a : b; }; const auto max2 = [&](u32 a, u32 b) { - if (nan(a)) return b; if (nan(b)) return a; + if (nan(a)) return b; if (zero(a) && zero(b)) return a & b; return value(a) >= value(b) ? a : b; }; @@ -27922,7 +27923,8 @@ TestCase VectorMinMaxF32RuntimeEdges(u32 wave) { {0x40000000u, 0x7fa00001u, 0xc0000000u}, {0x80000000u, 0x00000000u, 0x80000000u}, {0x00000000u, 0x80000000u, 0x00000000u}, {0x80000000u, 0x80000000u, 0x00000000u}, {0xff800000u, 0x7f800000u, 0x00000000u}, {0x7fc00000u, 0x00000000u, 0x80000000u}, - {0xffc05678u, 0x3f800000u, 0x7fa00001u}, {0xbf800000u, 0xc0000000u, 0x7fc01234u}}; + {0xffc05678u, 0x3f800000u, 0x7fa00001u}, {0xbf800000u, 0xc0000000u, 0x7fc01234u}, + {0x7fc01234u, 0xffc05678u, 0x7fa00001u}, {0xffc05678u, 0x7fc01234u, 0x3f800000u}}; TestCase test; test.name = "VectorMinMaxF32RuntimeEdges"; test.has_compute_info = true;