From 9f88043ebec87bc5fbdb62d380ca1ea1ba168f0c Mon Sep 17 00:00:00 2001 From: nmzik Date: Sat, 17 Oct 2026 12:23:12 +0200 Subject: [PATCH] shader: use compact integer RTZ half conversion --- .../backend/spirv/spirvEmitterAlu.cpp | 59 +++++++++++++------ tests/ShaderRecompilerComputeTests.cpp | 51 ++++++++++++++++ 2 files changed, 93 insertions(+), 17 deletions(-) diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterAlu.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterAlu.cpp index 3a3eb639a..0cdf1c7f4 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterAlu.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterAlu.cpp @@ -67,6 +67,44 @@ uint32_t EmitExt(EmitterState& state, uint32_t type, uint32_t opcode, return result; } +uint32_t EmitF32ToF16RtzBits(EmitterState& state, uint32_t value) { + // Native half conversion causes regressions; use compact integer RTZ conversion. + const auto bits = EmitBitcastF32ToU32(state, value); + const auto abs = EmitAndConstant(state, bits, 0x7fffffffu); + const auto sign = EmitAndConstant( + state, Binary(state, spv::OpShiftRightLogical, TypeU32(state), bits, ConstantU32(state, 16)), + 0x8000u); + const auto exp = Binary(state, spv::OpShiftRightLogical, TypeU32(state), abs, + ConstantU32(state, 23)); + const auto mant = EmitAndConstant(state, abs, 0x007fffffu); + + // Align the hidden bit to the half subnormal mantissa. Even unused shifts must be < 32. + const auto shift = EmitUMin32( + state, Binary(state, spv::OpISub, TypeU32(state), ConstantU32(state, 126), exp), + ConstantU32(state, 31)); + const auto sub = Binary(state, spv::OpShiftRightLogical, TypeU32(state), + EmitOrU32(state, mant, ConstantU32(state, 0x00800000u)), shift); + + // Drop 13 mantissa bits and rebias the exponent; finite overflow saturates to max half. + const auto truncated = Binary(state, spv::OpShiftRightLogical, TypeU32(state), abs, + ConstantU32(state, 13)); + const auto normal = EmitUMin32( + state, Binary(state, spv::OpISub, TypeU32(state), truncated, ConstantU32(state, 0x1c000u)), + ConstantU32(state, 0x7bffu)); + const auto finite = Select(state, TypeU32(state), + EmitCompareU32Constant(state, spv::OpULessThan, abs, 0x38800000u), + sub, normal); + const auto nan = EmitOrU32(state, EmitAndConstant(state, truncated, 0x03ffu), + ConstantU32(state, 0x7e00u)); + const auto special = Select(state, TypeU32(state), + EmitCompareU32Constant(state, spv::OpIEqual, mant, 0), + ConstantU32(state, 0x7c00u), nan); + const auto magnitude = Select( + state, TypeU32(state), + EmitCompareU32Constant(state, spv::OpUGreaterThanEqual, abs, 0x7f800000u), special, finite); + return EmitOrU32(state, sign, magnitude); +} + uint32_t EmitF32ToU32(EmitterState& state, uint32_t src, bool signed_value) { const auto trunc = EmitTruncF32Value(state, src); const auto converted_raw = state.builder.AllocateId(); @@ -210,23 +248,10 @@ uint32_t EmitCompositeExtractU32x2(EmitterState& state, uint32_t arg0, IR::Value } uint32_t EmitPackFloat2x16Rtz(EmitterState& state, uint32_t arg0, uint32_t arg1) { - const auto pair = state.builder.AllocateId(); - state.builder.AddFunction(spv::OpCompositeConstruct, TypeF32Vector(state, 2), pair, arg0, arg1); - const auto packed = EmitPackHalf2x16(state, pair); - const auto back = EmitExt(state, TypeF32Vector(state, 2), GLSLstd450UnpackHalf2x16, {packed}); - auto result = packed; - for (uint32_t i = 0; i < 2; i++) { - const auto rounded = state.builder.AllocateId(); - state.builder.AddFunction(spv::OpCompositeExtract, TypeF32(state), rounded, back, i); - const auto above = - Binary(state, spv::OpFOrdGreaterThan, TypeBool(state), - EmitExt(state, TypeF32(state), GLSLstd450FAbs, {rounded}), - EmitExt(state, TypeF32(state), GLSLstd450FAbs, {i == 0 ? arg0 : arg1})); - result = Binary(state, spv::OpISub, TypeU32(state), result, - Select(state, TypeU32(state), above, - ConstantU32(state, i == 0 ? 1u : 0x10000u), ConstantU32(state, 0))); - } - return result; + const auto low = EmitF32ToF16RtzBits(state, arg0); + const auto high = Binary(state, spv::OpShiftLeftLogical, TypeU32(state), + EmitF32ToF16RtzBits(state, arg1), ConstantU32(state, 16)); + return EmitOrU32(state, low, high); } uint32_t EmitFPSaturate32(EmitterState& state, uint32_t arg0) { diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index fdfe1b873..fec5f7279 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -25877,6 +25877,49 @@ TestCase CvtPkrtzF16F32SubnormalRoundsTowardZero() { O::S_ENDPGM}}; } +TestCase CvtPkrtzF16F32RuntimeEdges() { + using O = ShaderOpcode; + // Independent expected half encodings, including truncation boundaries and NaN payloads. + constexpr std::array, 31> inputs{{ + {0x00000000u, 0x0000u}, {0x80000000u, 0x8000u}, + {0x00000001u, 0x0000u}, {0x007fffffu, 0x0000u}, + {0x00800000u, 0x0000u}, {0x32ffffffu, 0x0000u}, + {0x33000000u, 0x0000u}, {0x337fffffu, 0x0000u}, + {0x33800000u, 0x0001u}, {0x33c00000u, 0x0001u}, + {0x34000000u, 0x0002u}, {0x387fc000u, 0x03ffu}, + {0x387fe000u, 0x03ffu}, {0x387fffffu, 0x03ffu}, + {0x38800000u, 0x0400u}, {0x38802000u, 0x0401u}, + {0x3e22f983u, 0x3117u}, {0x3f800000u, 0x3c00u}, + {0x3f801000u, 0x3c00u}, {0x3f801001u, 0x3c00u}, + {0x3f802000u, 0x3c01u}, {0x477fe000u, 0x7bffu}, + {0x477ff000u, 0x7bffu}, {0x477fffffu, 0x7bffu}, + {0x47800000u, 0x7bffu}, {0x7f7fffffu, 0x7bffu}, + {0x7f800000u, 0x7c00u}, {0x7f800001u, 0x7e00u}, + {0x7fa02000u, 0x7f01u}, {0x7fc12000u, 0x7e09u}, + {0x7fffffffu, 0x7fffu}, + }}; + TestCase test; + test.name = "CvtPkrtzF16F32RuntimeEdges"; + for (const auto &input : inputs) + test.initial.insert(test.initial.end(), {input[0], input[0] ^ 0x80000000u}); + test.expected = test.initial; + for (u32 i = 0; i < inputs.size(); ++i) { + for (u32 reg = 0; reg < 2; ++reg) { + AppendVMovU32(&test.code, 30, (i * 2u + reg) * 4u); + AppendBufferLoadDword(&test.code, reg, 30); + } + test.code.push_back(EncodeVop2(0x2f, 2, Vgpr(0), 1)); + AppendStoreVgpr(&test.code, 2, static_cast(test.expected.size())); + const auto half = inputs[i][1]; + test.expected.push_back(half | ((half ^ 0x8000u) << 16)); + } + AppendEnd(&test.code); + test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_DWORD, O::V_CVT_PKRTZ_F16_F32, + O::BUFFER_STORE_DWORD, O::S_ENDPGM}; + test.forbidden_spirv = {"PackHalf2x16", "UnpackHalf2x16"}; + return test; +} + TestCase CvtPkrtzF16F32SdwaAndOutputModifiers() { using O = ShaderOpcode; @@ -42213,6 +42256,7 @@ std::vector MakeCases() { AddCase(Vop3pInt16Ops); AddCase(CvtPkU8F32PacksSelectedByte); AddCase(CvtPkrtzF16F32SubnormalRoundsTowardZero); + AddCase(CvtPkrtzF16F32RuntimeEdges); AddCase(CvtPkrtzF16F32SdwaAndOutputModifiers); AddCase(PackedMinMaxF16NanAndSignedZeroEdges); AddCase(VectorMinMaxF16Ops); @@ -48029,6 +48073,13 @@ int main(int argc, char **argv) { std::setvbuf(stdout, nullptr, _IONBF, 0); EnsureConfigInitialized(); CheckLeastRecentlyUsedCacheOrdering(); + if (argc == 2 && std::strcmp(argv[1], "--cvt-pkrtz-only") == 0) { + VulkanHarness vulkan; + RunCase(&vulkan, CvtPkrtzF16F32SubnormalRoundsTowardZero()); + RunCase(&vulkan, CvtPkrtzF16F32RuntimeEdges()); + RunCase(&vulkan, CvtPkrtzF16F32SdwaAndOutputModifiers()); + return 0; + } if (argc == 2 && std::strcmp(argv[1], "--thread-dimensions-only") == 0) { VulkanHarness vulkan; CheckComputeThreadDimensions(vulkan);