From e864cb20bd4f14fbc7e9c7b0c1f3a70fc56cd9b2 Mon Sep 17 00:00:00 2001 From: TheCruZ Date: Sun, 25 Oct 2026 00:36:03 +0200 Subject: [PATCH] shader: clamp V_FRACT_F16/F32/F64 results below one --- .../backend/spirv/spirvEmitterInstructions.h | 1 + .../recompiler/frontend/translate/Float.cpp | 28 ++++++++++++ .../frontend/translate/Translator.h | 1 + .../recompiler/frontend/translate/Vector.cpp | 6 +-- .../recompiler/ir/opcodes/ValueOpcodes.inc | 1 + tests/ShaderRecompilerComputeTests.cpp | 44 ++++++++++++++++--- 6 files changed, 73 insertions(+), 8 deletions(-) diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInstructions.h b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInstructions.h index a99c7a7b5..d38814fd9 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInstructions.h +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInstructions.h @@ -56,6 +56,7 @@ EMIT_NATIVE(BitFieldSExtract, OpBitFieldSExtract, U32, uint32_t, uint32_t, uint3 EMIT_NATIVE(SelectU1, OpSelect, U1, uint32_t, uint32_t, uint32_t) EMIT_NATIVE(SelectU32, OpSelect, U32, uint32_t, uint32_t, uint32_t) EMIT_NATIVE(SelectF32, OpSelect, F32, uint32_t, uint32_t, uint32_t) +EMIT_NATIVE(SelectF64, OpSelect, F64, uint32_t, uint32_t, uint32_t) EMIT_NATIVE(IAdd32, OpIAdd, U32, uint32_t, uint32_t) EMIT_NATIVE(ISub32, OpISub, U32, uint32_t, uint32_t) EMIT_NATIVE(IMul32, OpIMul, U32, uint32_t, uint32_t) diff --git a/src/graphics/shader/recompiler/frontend/translate/Float.cpp b/src/graphics/shader/recompiler/frontend/translate/Float.cpp index cb8cb276d..76d73819b 100644 --- a/src/graphics/shader/recompiler/frontend/translate/Float.cpp +++ b/src/graphics/shader/recompiler/frontend/translate/Float.cpp @@ -1,6 +1,7 @@ #include "graphics/shader/recompiler/frontend/translate/Translator.h" #include +#include #include namespace Libs::Graphics::ShaderRecompiler::Frontend { @@ -118,6 +119,33 @@ void Translator::FloatUnary(const Decoder::Instruction& inst, IR::ValueOpcode op WriteOperand(DestinationOperand(inst), ir.Emit(opcode, {ReadOperand(inst.src0, type)})); } +void Translator::V_FRACT(const Decoder::Instruction& inst, IR::Type type) { + // x - floor(x) rounds to 1.0 for a tiny negative x; v_fract returns the largest value below + // one instead. The ordered compare lets NaN through. + const auto destination = DestinationOperand(inst); + if (type == IR::Type::F64) { + const auto result = ir.Emit(IR::ValueOpcode::FPFract64, {ReadOperand(inst.src0, type)}); + const auto below_one = ir.Emit(IR::ValueOpcode::BitCastF64U64, + {IR::Value(uint64_t {0x3fefffffffffffff})}); + const auto above = ir.Emit(IR::ValueOpcode::FPOrdGreaterThan64, {result, below_one}); + WriteOperand(destination, ir.Emit(IR::ValueOpcode::SelectF64, {above, below_one, result})); + return; + } + const bool half = type == IR::Type::F16; + const auto source = half ? ReadF16AsF32(inst.src0) : IR::F32(ReadOperand(inst.src0, type)); + const auto result = IR::F32(ir.Emit(IR::ValueOpcode::FPFract32, {source})); + // Clamp in the destination precision: 0x3f7fe000 is the half 0x3bff. + const auto below_one = + IR::F32(IR::Value::F32(std::bit_cast(half ? 0x3f7fe000u : 0x3f7fffffu))); + const auto above = IR::U1(ir.Emit(IR::ValueOpcode::FPOrdGreaterThan32, {result, below_one})); + const auto clamped = SelectF32(above, below_one, result); + if (half) { + WriteF16(destination, clamped); + } else { + WriteOperand(destination, clamped); + } +} + void Translator::FloatBinary(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool reverse) { std::array args; diff --git a/src/graphics/shader/recompiler/frontend/translate/Translator.h b/src/graphics/shader/recompiler/frontend/translate/Translator.h index e82a1ead7..270d7d90f 100644 --- a/src/graphics/shader/recompiler/frontend/translate/Translator.h +++ b/src/graphics/shader/recompiler/frontend/translate/Translator.h @@ -153,6 +153,7 @@ private: void Float16Ternary(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool accumulator, bool mix); void FloatUnary(const Decoder::Instruction& inst, IR::ValueOpcode opcode); + void V_FRACT(const Decoder::Instruction& inst, IR::Type type); void FloatBinary(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool reverse); void FloatTernary(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool accumulator, bool mix); diff --git a/src/graphics/shader/recompiler/frontend/translate/Vector.cpp b/src/graphics/shader/recompiler/frontend/translate/Vector.cpp index f95b82a75..492a470d2 100644 --- a/src/graphics/shader/recompiler/frontend/translate/Vector.cpp +++ b/src/graphics/shader/recompiler/frontend/translate/Vector.cpp @@ -563,7 +563,7 @@ void Translator::EmitVector(const Decoder::Instruction& inst) { case O::V_CEIL_F16: return Float16Unary(inst, IR::ValueOpcode::FPCeil32, false); case O::V_TRUNC_F16: return Float16Unary(inst, IR::ValueOpcode::FPTrunc32, false); case O::V_RNDNE_F16: return Float16Unary(inst, IR::ValueOpcode::FPRoundEven32, false); - case O::V_FRACT_F16: return Float16Unary(inst, IR::ValueOpcode::FPFract32, false); + case O::V_FRACT_F16: return V_FRACT(inst, IR::Type::F16); case O::V_SIN_F16: return Float16Trig(inst, IR::ValueOpcode::FPSin); case O::V_COS_F16: return Float16Trig(inst, IR::ValueOpcode::FPCos); case O::V_MIN3_F16: return Float16Ternary(inst, IR::ValueOpcode::FPMinTri32, false, false); @@ -574,8 +574,8 @@ void Translator::EmitVector(const Decoder::Instruction& inst) { case O::V_RCP_F32: return FloatUnary(inst, IR::ValueOpcode::FPRecip32); case O::V_RCP_F64: return FloatUnary(inst, IR::ValueOpcode::FPRecip64); case O::V_RCP_IFLAG_F32: return FloatUnary(inst, IR::ValueOpcode::FPRecipIFlag32); - case O::V_FRACT_F32: return FloatUnary(inst, IR::ValueOpcode::FPFract32); - case O::V_FRACT_F64: return FloatUnary(inst, IR::ValueOpcode::FPFract64); + case O::V_FRACT_F32: return V_FRACT(inst, IR::Type::F32); + case O::V_FRACT_F64: return V_FRACT(inst, IR::Type::F64); case O::V_TRUNC_F32: return FloatUnary(inst, IR::ValueOpcode::FPTrunc32); case O::V_TRUNC_F64: return FloatUnary(inst, IR::ValueOpcode::FPTrunc64); case O::V_CEIL_F32: return FloatUnary(inst, IR::ValueOpcode::FPCeil32); diff --git a/src/graphics/shader/recompiler/ir/opcodes/ValueOpcodes.inc b/src/graphics/shader/recompiler/ir/opcodes/ValueOpcodes.inc index 6acda598e..1b0dcb9b2 100644 --- a/src/graphics/shader/recompiler/ir/opcodes/ValueOpcodes.inc +++ b/src/graphics/shader/recompiler/ir/opcodes/ValueOpcodes.inc @@ -96,6 +96,7 @@ VALUE_OPCODE(WqmU64, U64, U64) // Core value operations used to expand architectural side effects. VALUE_OPCODE(SelectU1, U1, U1, U1, U1) VALUE_OPCODE(SelectF32, F32, U1, F32, F32) +VALUE_OPCODE(SelectF64, F64, U1, F64, F64) VALUE_OPCODE(IAdd32, U32, U32, U32) VALUE_OPCODE(IAdd64, U64, U64, U64) VALUE_OPCODE(ISub32, U32, U32, U32) diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index 1064d0b21..492f6dbff 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -27670,16 +27670,16 @@ TestCase VectorSpecialF16Ops() { TestCase VectorFractF16CapturedAndEdges() { using O = ShaderOpcode; - // RDNA2 section 12.8: x - floor(x), rounded to half precision. + // x - floor(x) rounded to half precision, clamped to 0x3bff below one. // Load at runtime to exercise the emitted operation, including half subnormals. const std::array inputs{ 0x3d00u, 0xbd00u, 0x0000u, 0x8000u, 0x0001u, 0x8001u, 0x03ffu, 0x83ffu, 0x7bffu, 0xfbffu, 0x3800u, 0xb800u, 0x8c00u, 0x8c01u, 0x7c00u, 0xfc00u, 0x7e55u, 0x7d01u}; const std::array fractions{ - 0x3400u, 0x3a00u, 0x0000u, 0x0000u, 0x0001u, 0x3c00u, - 0x03ffu, 0x3c00u, 0x0000u, 0x0000u, 0x3800u, 0x3800u, - 0x3c00u, 0x3bffu, 0x7e00u, 0x7e00u, 0x7e00u, 0x7e00u}; + 0x3400u, 0x3a00u, 0x0000u, 0x0000u, 0x0001u, 0x3bffu, + 0x03ffu, 0x3bffu, 0x0000u, 0x0000u, 0x3800u, 0x3800u, + 0x3bffu, 0x3bffu, 0x7e00u, 0x7e00u, 0x7e00u, 0x7e00u}; TestCase test; test.name = "VectorFractF16CapturedAndEdges"; for (u32 bits : inputs) { @@ -27710,6 +27710,37 @@ TestCase VectorFractF16CapturedAndEdges() { return test; } +// V_FRACT_F32 clamps to 0x3f7fffff where x - floor(x) rounds to 1.0, before output modifiers. +TestCase VectorFractF32Edges() { + using O = ShaderOpcode; + + constexpr std::array, 4> cases{{ + {0xb3800000u, 0, 0x3f7fffffu}, // -2^-24: exact + {0xb3000000u, 0, 0x3f7fffffu}, // -2^-25: the tie rounds to 1.0 + {0xb3000000u, 1, 0x3fffffffu}, // omod *2 applies after the clamp + {0x7fc00001u, 0, 0x7fc00000u}}}; // Quiet NaN class + TestCase test; + test.name = "VectorFractF32Edges"; + for (const auto &[source, omod, expected] : cases) test.initial.push_back(source); + test.expected = test.initial; + auto &code = test.code; + for (u32 i = 0; i < cases.size(); i++) { + AppendVMovU32(&code, 30, i * 4u); + AppendBufferLoadDword(&code, 7, 30); + AppendVop3(&code, 0x1a0, 9, Vgpr(7), 0, 0, 0, 0, false, cases[i][1]); + if (i + 1u == cases.size()) { + code.push_back(EncodeVop2(0x1b, 9, 255u, 9)); + code.push_back(0x7fc00000u); + } + AppendStoreVgpr(&code, 9, static_cast(cases.size()) + i); + test.expected.push_back(cases[i][2]); + } + AppendEnd(&code); + test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_DWORD, O::V_FRACT_F32, + O::V_AND_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; + return test; +} + TestCase VectorFractF16Modifiers() { using O = ShaderOpcode; @@ -29628,7 +29659,7 @@ TestCase VectorFractF64CapturedAndEdges() { {0x4320000000000001ull, 0x3fe0000000000000ull}, // 2^51 + .5 {0, 0}, {0x8000000000000000ull, 0}, {0x401c000000000000ull, 0}, {0xc01c000000000000ull, 0}, - {1, 1}, {0x8000000000000001ull, 0x3ff0000000000000ull}, + {1, 1}, {0x8000000000000001ull, 0x3fefffffffffffffull}, // Clamped below one. {0x000fffffffffffffull, 0x000fffffffffffffull}, {0x7fefffffffffffffull, 0}, {0x7ff0000000000000ull, 0x7ff8000000000000ull}, @@ -44123,6 +44154,7 @@ std::vector MakeCases() { AddCase(VectorMinMaxMed3F16Ops); AddCase(VectorSpecialF16Ops); AddCase(VectorFractF16CapturedAndEdges); + AddCase(VectorFractF32Edges); AddCase(VectorFractF16Modifiers); AddCase(VectorCosF16CapturedSdwaAndEdges); AddCase(VectorSinF16SdwaAndEdges); @@ -50749,6 +50781,8 @@ int main(int argc, char **argv) { if (argc == 2 && std::strcmp(argv[1], "--fract-f16-only") == 0) { VulkanHarness vulkan; RunCase(&vulkan, VectorFractF16CapturedAndEdges()); + RunCase(&vulkan, VectorFractF32Edges()); + RunCase(&vulkan, VectorFractF64CapturedAndEdges()); RunCase(&vulkan, VectorFractF16Modifiers()); RunCase(&vulkan, NativeAndSdwa16BitDestinationWrites()); RunCase(&vulkan, VectorSpecialF16Ops());