From 41de2fd64ebff90c717eff50dc6b96c0d89b3d39 Mon Sep 17 00:00:00 2001 From: nmzik Date: Wed, 28 Oct 2026 22:19:02 +0100 Subject: [PATCH] shader: support five VOP3 float conversion forms --- CMakeLists.txt | 2 + .../frontend/decode/VectorAluOps.cpp | 11 +++ tests/ShaderRecompilerComputeTests.cpp | 82 +++++++++++++++++++ tests/shaderCfgTests.cpp | 45 ++++++++++ 4 files changed, 140 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 059ae1945..a5c547bc1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -692,6 +692,8 @@ if(BUILD_TESTING) COMMAND $ --tessellation-only) add_test(NAME shader_recompiler_float_min_max COMMAND $ --float-min-max-only) + add_test(NAME shader_recompiler_vop3_conversions + COMMAND $ --vop3-conversions-only) add_test(NAME shader_recompiler_alignbyte COMMAND $ --alignbyte-only) add_test(NAME graphics_pipeline_rasterization diff --git a/src/graphics/shader/recompiler/frontend/decode/VectorAluOps.cpp b/src/graphics/shader/recompiler/frontend/decode/VectorAluOps.cpp index c9ca8d5c8..6a9865242 100644 --- a/src/graphics/shader/recompiler/frontend/decode/VectorAluOps.cpp +++ b/src/graphics/shader/recompiler/frontend/decode/VectorAluOps.cpp @@ -157,6 +157,10 @@ constexpr OpcodeMap VOP3_ENCODED_VOP1_OPCODE_LIST[] = { {0x04u, Opcode::V_CVT_F64_I32}, {0x0fu, Opcode::V_CVT_F32_F64}, {0x10u, Opcode::V_CVT_F64_F32}, + {0x11u, Opcode::V_CVT_F32_UBYTE0}, + {0x12u, Opcode::V_CVT_F32_UBYTE1}, + {0x13u, Opcode::V_CVT_F32_UBYTE2}, + {0x14u, Opcode::V_CVT_F32_UBYTE3}, {0x16u, Opcode::V_CVT_F64_U32}, {0x17u, Opcode::V_TRUNC_F64}, {0x18u, Opcode::V_CEIL_F64}, @@ -167,6 +171,7 @@ constexpr OpcodeMap VOP3_ENCODED_VOP1_OPCODE_LIST[] = { {0x07u, Opcode::V_CVT_U32_F32}, {0x08u, Opcode::V_CVT_I32_F32}, {0x0au, Opcode::V_CVT_F16_F32}, + {0x0bu, Opcode::V_CVT_F32_F16}, {0x0cu, Opcode::V_CVT_RPI_I32_F32}, {0x0du, Opcode::V_CVT_FLR_I32_F32}, {0x0eu, Opcode::V_CVT_OFF_F32_I4}, @@ -1557,6 +1562,12 @@ bool HasUnsupportedNativeVop3Modifiers(Opcode opcode, bool permlane, bool mad_mi } switch (opcode) { case Opcode::V_LDEXP_F32: return (abs & ~1u) != 0u || op_sel != 0u || (neg & ~1u) != 0u; + case Opcode::V_CVT_F32_UBYTE0: + case Opcode::V_CVT_F32_UBYTE1: + case Opcode::V_CVT_F32_UBYTE2: + case Opcode::V_CVT_F32_UBYTE3: + // Byte conversions ignore input modifiers and support all output modifiers. + return op_sel != 0u; case Opcode::V_INTERP_P1LL_F16: case Opcode::V_INTERP_P2_F16: return abs != 0u || (op_sel & 0x7u) != 0u || clamp != 0u || omod != 0u || neg != 0u; diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index 55c7088b1..f73329078 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -29658,6 +29658,79 @@ TestCase VectorMed3F32NanUsesMin3Path(u32 wave) { return test; } +TestCase Vop3FloatConversions(u32 wave_size) { + using O = ShaderOpcode; + struct ConversionCase { + u32 opcode, input, expected; + u32 abs = 0, neg = 0, omod = 0; + bool clamp = false; + u32 exec = 1; + }; + const ConversionCase cases[] = { + {0x18b, 0xabcd3e00u, 0x3fc00000u}, // Only the low half: 1.5. + {0x18b, 0x7c00c100u, 0xc0200000u}, // -2.5, despite +inf in the high half. + {0x18b, 0x12340000u, 0x00000000u}, + {0x18b, 0x12348000u, 0x80000000u}, + {0x18b, 0x12347c00u, 0x7f800000u}, + {0x18b, 0x1234fc00u, 0xff800000u}, + {0x18b, 0x1234c100u, 0x40200000u, 1}, + {0x18b, 0x12343e00u, 0xbfc00000u, 0, 1}, + {0x18b, 0x1234c100u, 0xc0200000u, 1, 1}, + {0x18b, 0x12343e00u, 0x40400000u, 0, 0, 1}, + {0x18b, 0x12343e00u, 0x40c00000u, 0, 0, 2}, + {0x18b, 0x12343e00u, 0x3f400000u, 0, 0, 3}, + {0x18b, 0x12343e00u, 0x3f800000u, 0, 0, 0, true}, + {0x18b, 0x12343e00u, 0x00000000u, 0, 1, 0, true}, + {0x18b, 0x12347e00u, 0x00000000u, 0, 0, 0, true}, // NaN clamp. + {0x18b, 0x12343e00u, 0x12343e00u, 0, 0, 0, false, 0}, + {0x191, 0xff807f00u, 0x00000000u}, + {0x192, 0xff807f00u, 0x42fe0000u}, + {0x193, 0xff807f00u, 0x43000000u}, + {0x194, 0xff807f00u, 0x437f0000u}, + {0x191, 0xff807f00u, 0x00000000u, 1, 1, 1}, + {0x192, 0xff807f00u, 0x43fe0000u, 1, 1, 2}, // 127 * 4; input modifiers ignored. + {0x193, 0xff807f00u, 0x42800000u, 1, 1, 3}, // 128 / 2. + {0x194, 0xff807f00u, 0x3f800000u, 1, 1, 0, true}, + {0x194, 0xff807f00u, 0xff807f00u, 0, 0, 0, false, 0}, + }; + TestCase test; + test.name = wave_size == 32 ? "Vop3FloatConversionsWave32" : "Vop3FloatConversionsWave64"; + test.compute_info.wave_size = wave_size; + test.has_compute_info = true; + for (const auto &entry : cases) test.initial.push_back(entry.input); + test.expected = test.initial; + auto &code = test.code; + for (u32 i = 0; i < std::size(cases); ++i) { + const auto &entry = cases[i]; + AppendVMovU32(&code, 30, i * 4u); + AppendBufferLoadDword(&code, 2, 30); // Runtime source aliases the destination. + code.push_back(EncodeSMovB32(126, InlineU32(entry.exec))); + AppendVop3(&code, entry.opcode, 2, Vgpr(2), 0, 0, entry.abs, 0, + entry.clamp, entry.omod, entry.neg); + code.push_back(EncodeSMovB32(126, InlineU32(1))); + AppendStoreVgpr(&code, 2, static_cast(test.expected.size())); + test.expected.push_back(entry.expected); + } + // Exercise scalar, literal, and inline sources for every new encoding too. + AppendSMovLiteral(&code, 12, 0x44333e00u); + const u32 scalar_results[] = {0x3fc00000u, 0, 0x42780000u, 0x424c0000u, 0x42880000u}; + const u32 opcodes[] = {0x18b, 0x191, 0x192, 0x193, 0x194}; + for (u32 i = 0; i < std::size(opcodes); ++i) { + for (const u32 source : {12u, 255u, InlineU32(0)}) { + AppendVop3(&code, opcodes[i], 2, source, 0); + if (source == 255u) code.push_back(0x44333e00u); + AppendStoreVgpr(&code, 2, static_cast(test.expected.size())); + test.expected.push_back(source == InlineU32(0) ? 0u : scalar_results[i]); + } + } + AppendEnd(&code); + test.opcodes = {O::V_MOV_B32, O::S_MOV_B32, O::BUFFER_LOAD_DWORD, + O::V_CVT_F32_F16, O::V_CVT_F32_UBYTE0, O::V_CVT_F32_UBYTE1, + O::V_CVT_F32_UBYTE2, O::V_CVT_F32_UBYTE3, + O::BUFFER_STORE_DWORD, O::S_ENDPGM}; + return test; +} + TestCase VectorFloatConversionOps() { using O = ShaderOpcode; @@ -46118,6 +46191,8 @@ std::vector MakeCases() { cases.push_back(VectorMed3F32NanUsesMin3Path(64)); cases.push_back(VectorMinMaxF32RuntimeEdges(32)); cases.push_back(VectorMinMaxF32RuntimeEdges(64)); + cases.push_back(Vop3FloatConversions(32)); + cases.push_back(Vop3FloatConversions(64)); AddCase(VectorFloatConversionOps); AddCase(VectorFrexpF32Edges); AddCase(CvtF32ToIntSaturatesNaNAndOutOfRange); @@ -52446,6 +52521,13 @@ int main(int argc, char **argv) { std::setvbuf(stdout, nullptr, _IONBF, 0); EnsureConfigInitialized(); CheckLeastRecentlyUsedCacheOrdering(); + if (argc == 2 && std::strcmp(argv[1], "--vop3-conversions-only") == 0) { + VulkanHarness vulkan; + RunCase(&vulkan, Vop3FloatConversions(32)); + RunCase(&vulkan, Vop3FloatConversions(64)); + RunCase(&vulkan, VectorFloatConversionOps()); + return 0; + } if (argc == 2 && std::strcmp(argv[1], "--cvt-pkrtz-only") == 0) { VulkanHarness vulkan; RunCase(&vulkan, CvtPkrtzF16F32SubnormalRoundsTowardZero()); diff --git a/tests/shaderCfgTests.cpp b/tests/shaderCfgTests.cpp index ac7b1428e..5815e2321 100644 --- a/tests/shaderCfgTests.cpp +++ b/tests/shaderCfgTests.cpp @@ -2397,6 +2397,50 @@ void TestNewShaderRecompilerVop3LaneReadDestinationEncoding() { CheckSpirvBinaryValidates(result.spirv); } +void TestVop3FloatConversionDecode() { + namespace Decoder = ShaderRecompiler::Decoder; + const std::pair conversions[] = { + {0x18b, Decoder::Opcode::V_CVT_F32_F16}, + {0x191, Decoder::Opcode::V_CVT_F32_UBYTE0}, + {0x192, Decoder::Opcode::V_CVT_F32_UBYTE1}, + {0x193, Decoder::Opcode::V_CVT_F32_UBYTE2}, + {0x194, Decoder::Opcode::V_CVT_F32_UBYTE3}, + }; + for (const auto &[encoding, opcode] : conversions) { + for (const uint32_t source : {261u, 12u, 128u, 255u}) { + const uint32_t code[] = { + EncodeVop3Word0(encoding, 20, 0, 1, true), + EncodeVop3Word1(source, 0, 0) | (2u << 27u) | (1u << 29u), + 0xa5a53e00u, + }; + Decoder::Instruction inst; + Decoder::DecodeInstruction(code, 0, inst); + Check(inst.opcode == opcode && inst.family == Decoder::Family::VOP3 && + inst.src_count == 1 && inst.word_count == (source == 255u ? 3u : 2u), + "VOP3 conversion lost its opcode, unary source, or literal length"); + Check(inst.dst.kind == Decoder::OperandKind::Vgpr && inst.dst.reg == 20 && + inst.dst.sdwa_sel == 6 && inst.dst.clamp && inst.dst.omod == 2, + "VOP3 conversion lost its full-width destination or output modifiers"); + const bool half = opcode == Decoder::Opcode::V_CVT_F32_F16; + Check(inst.src0.absolute == half && inst.src0.negate == half, + "VOP3 conversion must apply half-float modifiers and ignore byte modifiers"); + Decoder::Operand expected_source; + Decoder::DecodeScalarSource(source, 0, expected_source); + Check(inst.src0.kind == expected_source.kind && inst.src0.reg == expected_source.reg && + (source != 255u || inst.src0.value == code[2]), + "VOP3 conversion changed its source register or literal value"); + } + for (const uint32_t op_sel : {1u, 2u, 4u, 8u}) { + const uint32_t code[] = {EncodeVop3Word0(encoding, 20, op_sel), + EncodeVop3Word1(261, 0, 0)}; + Decoder::Instruction inst; + Decoder::DecodeInstruction(code, 0, inst); + Check(inst.opcode == Decoder::Opcode::UNSUPPORTED, + "VOP3 conversion accepted unsupported OP_SEL bits"); + } + } +} + void TestNewShaderRecompilerMoreAluFamilies() { const uint32_t shader[] = { EncodeSopk(0x00, 9, 7), // s_movk_i32 s9, 7 @@ -17158,6 +17202,7 @@ int main() { using namespace Libs::Graphics; EnsureConfigInitialized(); + TestVop3FloatConversionDecode(); TestRayTracingInstructions(); TestResourceDescriptorClassification(); TestShaderBufferResourceSize();