From defb2f11d3805e898077102d7a043c5a0ce3d5ce Mon Sep 17 00:00:00 2001 From: nmzik Date: Mon, 19 Oct 2026 00:59:31 +0200 Subject: [PATCH] Share median classifications and preserve RDNA2 signed-zero selection --- .../backend/spirv/spirvEmitterAlu.cpp | 13 --- .../backend/spirv/spirvEmitterAluHelpers.cpp | 35 +++++++- tests/ShaderRecompilerComputeTests.cpp | 80 +++++++++++++------ 3 files changed, 88 insertions(+), 40 deletions(-) diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterAlu.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterAlu.cpp index 0cdf1c7f4..614880fa3 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterAlu.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterAlu.cpp @@ -160,19 +160,6 @@ uint32_t EmitFPMad32(EmitterState& state, uint32_t a, uint32_t b, uint32_t c) { return sum; } -uint32_t EmitFPMedTri32(EmitterState& state, uint32_t a, uint32_t b, uint32_t c) { - const auto min_ab = EmitMinMaxF32Value(state, a, b, false); - const auto min3 = EmitMinMaxF32Value(state, min_ab, c, false); - const auto max_ab = EmitMinMaxF32Value(state, a, b, true); - const auto high_min = EmitMinMaxF32Value(state, max_ab, c, false); - const auto median = EmitMinMaxF32Value(state, min_ab, high_min, true); - const auto nan_ab = Binary(state, spv::OpLogicalOr, TypeBool(state), - EmitClassifyF32(state, a).nan, EmitClassifyF32(state, b).nan); - const auto any_nan = - Binary(state, spv::OpLogicalOr, TypeBool(state), nan_ab, EmitClassifyF32(state, c).nan); - return Select(state, TypeF32(state), any_nan, min3, median); -} - uint32_t EmitFindUMsb64(EmitterState& state, uint32_t value) { // GLSL FindUMsb is restricted to 32-bit components. const auto unpacked = Unary(state, spv::OpBitcast, TypeU32Vector(state, 2), value); diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterAluHelpers.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterAluHelpers.cpp index b72f7e7f0..167ab6fff 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterAluHelpers.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterAluHelpers.cpp @@ -100,10 +100,9 @@ uint32_t EmitClassMaskF16(EmitterState& state, uint32_t bits, uint32_t mask) { return EmitClassMaskBits(state, bits, mask, true); } -uint32_t EmitMinMaxF32Value(EmitterState& state, uint32_t lhs, uint32_t rhs, bool max_value) { - const auto lhs_class = EmitClassifyF32(state, lhs); - const auto rhs_class = EmitClassifyF32(state, rhs); - +static uint32_t EmitMinMaxF32(EmitterState& state, uint32_t lhs, uint32_t rhs, + const F32Class& lhs_class, const F32Class& rhs_class, + bool max_value) { const auto numeric_cond = state.builder.AllocateId(); state.builder.AddFunction(max_value ? spv::OpFOrdGreaterThanEqual : spv::OpFOrdLessThan, TypeBool(state), numeric_cond, lhs, rhs); @@ -121,6 +120,34 @@ uint32_t EmitMinMaxF32Value(EmitterState& state, uint32_t lhs, uint32_t rhs, boo return EmitBitcastU32ToF32(state, result_bits); } +uint32_t EmitMinMaxF32Value(EmitterState& state, uint32_t lhs, uint32_t rhs, bool max_value) { + const auto lhs_class = EmitClassifyF32(state, lhs); + const auto rhs_class = EmitClassifyF32(state, rhs); + return EmitMinMaxF32(state, lhs, rhs, lhs_class, rhs_class, max_value); +} + +uint32_t EmitFPMedTri32(EmitterState& state, uint32_t a, uint32_t b, uint32_t c) { + const auto a_class = EmitClassifyF32(state, a); + const auto b_class = EmitClassifyF32(state, b); + const auto c_class = EmitClassifyF32(state, c); + const auto min_ab = EmitMinMaxF32(state, a, b, a_class, b_class, false); + const auto min_ab_class = EmitClassifyF32(state, min_ab); + const auto min3 = EmitMinMaxF32(state, min_ab, c, min_ab_class, c_class, false); + const auto max_ab = EmitMinMaxF32(state, a, b, a_class, b_class, true); + const auto max_ab_class = EmitClassifyF32(state, max_ab); + const auto max3 = EmitMinMaxF32(state, max_ab, c, max_ab_class, c_class, true); + const auto max_bc = EmitMinMaxF32(state, b, c, b_class, c_class, true); + const auto max_ac = EmitMinMaxF32(state, a, c, a_class, c_class, true); + // RDNA2 compares numerically against MAX3, including ties between signed zeros. + const auto a_is_max = Binary(state, spv::OpFOrdEqual, TypeBool(state), max3, a); + const auto b_is_max = Binary(state, spv::OpFOrdEqual, TypeBool(state), max3, b); + const auto median = Select(state, TypeF32(state), a_is_max, max_bc, + Select(state, TypeF32(state), b_is_max, max_ac, max_ab)); + const auto nan_ab = EmitLogicalOrBool(state, a_class.nan, b_class.nan); + const auto any_nan = EmitLogicalOrBool(state, nan_ab, c_class.nan); + return Select(state, TypeF32(state), any_nan, min3, median); +} + uint32_t EmitFlushF32DenormToSignedZero(EmitterState& state, uint32_t value) { const auto bits = state.builder.AllocateId(); const auto abs_bits = state.builder.AllocateId(); diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index 2960911f0..cf2df5f5e 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -27720,29 +27720,60 @@ TestCase VectorMinMaxF32NanAndSignedZeroEdges() { O::V_MAX3_F32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } -TestCase VectorMed3F32NanUsesMin3Path() { +TestCase VectorMed3F32NanUsesMin3Path(u32 wave) { using O = ShaderOpcode; - - std::vector code; - AppendVMovLiteral(&code, 0, 0x40000000u); - AppendVMovLiteral(&code, 1, 0x40400000u); - AppendVMovLiteral(&code, 2, 0x7fa00001u); - AppendVMovLiteral(&code, 3, 0x7fc00000u); - - AppendVop3(&code, 0x157, 10, Vgpr(0), Vgpr(1), Vgpr(2)); - AppendVop3(&code, 0x157, 11, Vgpr(0), Vgpr(1), Vgpr(3)); - AppendVop3(&code, 0x157, 12, Vgpr(2), Vgpr(0), Vgpr(1)); - - AppendStoreVgpr(&code, 10, 0); - AppendStoreVgpr(&code, 11, 1); - AppendStoreVgpr(&code, 12, 2); + // Runtime inputs keep every MED3 in the emitted module; compare raw result bits. + constexpr u32 cases[][4] = { + {0x40000000u, 0x40400000u, 0x7fa00001u, 0x40000000u}, + {0x40000000u, 0x40400000u, 0x7fc00000u, 0x40000000u}, + {0x7fa00001u, 0x40000000u, 0x40400000u, 0x40000000u}, + {0x40400000u, 0x7fc01234u, 0x40000000u, 0x40000000u}, + {0x80000000u, 0x00000000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x80000000u, 0xbf800000u, 0x80000000u}, + {0x80000000u, 0x80000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u}, + {0x80000000u, 0x00000000u, 0x80000000u, 0x00000000u}, + {0x00000000u, 0x80000000u, 0x80000000u, 0x80000000u}, + {0xbf800000u, 0x80000000u, 0x00000000u, 0x00000000u}, + {0xbf800000u, 0x00000000u, 0x80000000u, 0x80000000u}, + {0x80000000u, 0xbf800000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0xbf800000u, 0x80000000u, 0x80000000u}, + {0x80000000u, 0x00000000u, 0xbf800000u, 0x00000000u}, + {0x7fc01234u, 0x80000000u, 0x00000000u, 0x80000000u}, + {0x3f800000u, 0x40400000u, 0x40000000u, 0x40000000u}, + {0xc0400000u, 0xbf800000u, 0xc0000000u, 0xc0000000u}, + {0xff800000u, 0x7f800000u, 0x3f800000u, 0x3f800000u}, + {0x00000001u, 0x7f800000u, 0x7fc01234u, 0x00000001u}, + {0x80000001u, 0x80000001u, 0x00000000u, 0x80000001u}, + {0x7fc01234u, 0x7fa00001u, 0xffc05678u, 0xffc05678u}, + {0x7fa00001u, 0xffc05678u, 0x40000000u, 0x40000000u}}; + TestCase test; + test.name = "VectorMed3F32NanUsesMin3Path"; + test.has_compute_info = true; + test.compute_info.wave_size = wave; + test.compute_info.float_mode = 0xf0; // Preserve FP32 input/output denormals. + test.compute_info.threads_num[0] = test.compute_info.threads_num[1] = test.compute_info.threads_num[2] = 1u; + for (const auto &values : cases) + test.initial.insert(test.initial.end(), values, values + 3); + test.expected = test.initial; + auto &code = test.code; + for (u32 index = 0; index < std::size(cases); ++index) { + for (u32 operand = 0; operand < 3; ++operand) { + AppendVMovU32(&code, 30, (index * 3u + operand) * 4u); + AppendBufferLoadDword(&code, operand, 30); + } + AppendVop3(&code, 0x157, 10, Vgpr(0), Vgpr(1), Vgpr(2)); + AppendStoreVgpr(&code, 10, static_cast(test.expected.size())); + test.expected.push_back(cases[index][3]); + } AppendEnd(&code); - - return {"VectorMed3F32NanUsesMin3Path", - code, - {}, - {0x40000000u, 0x40000000u, 0x40000000u}, - {O::V_MOV_B32, O::V_MED3_F32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; + test.initial.resize(test.expected.size()); + test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_DWORD, O::V_MED3_F32, + O::BUFFER_STORE_DWORD, O::S_ENDPGM}; + test.ir_counts = {{"FPMedTri32", std::size(cases)}}; + // Measured 17,593/36,607 words; keep classification growth below a single MED3. + test.max_spirv_words = wave == 32u ? 17600u : 36620u; + return test; } TestCase VectorFloatConversionOps() { @@ -42807,7 +42838,8 @@ std::vector MakeCases() { AddCase(VectorFloatControlContractPreservesInfNan); AddCase(VectorFloatArithmeticOps); AddCase(VectorMinMaxF32NanAndSignedZeroEdges); - AddCase(VectorMed3F32NanUsesMin3Path); + cases.push_back(VectorMed3F32NanUsesMin3Path(32)); + cases.push_back(VectorMed3F32NanUsesMin3Path(64)); AddCase(VectorFloatConversionOps); AddCase(VectorFrexpF32Edges); AddCase(CvtF32ToIntSaturatesNaNAndOutOfRange); @@ -48691,8 +48723,10 @@ int main(int argc, char **argv) { RunCase(&vulkan, VectorCompareF32DenormalModes(mode)); } RunCase(&vulkan, VectorCompareClassF32()); - for (const u32 wave : {32u, 64u}) + for (const u32 wave : {32u, 64u}) { RunCase(&vulkan, VectorCompareClassF32ImmediateAndDynamicMasks(wave)); + RunCase(&vulkan, VectorMed3F32NanUsesMin3Path(wave)); + } RunCase(&vulkan, VectorCompareF16Ops()); return 0; }