From 42692dee11d7a6fe68261c50abba9ce3c71107d5 Mon Sep 17 00:00:00 2001 From: Karan Vora <52519664+karanvora2599@users.noreply.github.com> Date: Fri, 9 Oct 2026 13:38:56 -0400 Subject: [PATCH] perf(shader): fold the denormal flush into f32 compares against constants When f32 denormals are flushed, v_cmp_*_f32 compares flush(a) with flush(b). With one operand a constant, the flush of the other operand can go into the compare: a denormal and its flushed zero compare the same way with a NaN, an infinity or a normal constant, and comparing flush(x) with a zero is comparing x with +-2^-126 (flush(x) < 0 is x <= -2^-126, flush(x) == 0 is |x| < 2^-126, and so on, the unordered forms alike). emitFloatCompare reads both operands unflushed, finds a constant operand after its abs/neg modifiers, mirrors the predicate when the constant is first, and emits one compare. Two registers, f16 compares and modes that keep f32 denormals are unchanged. New test agc_driver_f32_compare_constant checks all 12 predicates against flush(a) op flush(b). --- .../tests/execution/F32CompareConstant.cpp | 183 ++++++++++++++++++ .../Translation/TranslationContext.hpp | 1 + .../Translation/src/CompareInstructions.cpp | 76 +++++++- 3 files changed, 258 insertions(+), 2 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/F32CompareConstant.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F32CompareConstant.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F32CompareConstant.cpp new file mode 100644 index 000000000..4b16ff31a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F32CompareConstant.cpp @@ -0,0 +1,183 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Words = 8; +alignas(256) std::array Input{ + 0x00000000u, 0x80000000u, 0x00000001u, 0x80000001u, 0x007fffffu, 0x807fffffu, 0x00800000u, 0x80800000u, + 0x00800001u, 0x80800001u, 0x3f000000u, 0xbf000000u, 0x3effffffu, 0x3f000001u, 0xc0800000u, 0xc07fffffu, + 0xc0800001u, 0x3f800000u, 0x7f800000u, 0xff800000u, 0x7fc00000u, 0xffc00001u, 0x7f800001u, 0x7f7fffffu, + 0xff7fffffu, 0x00400000u, 0x80400000u, 0x0000ffffu, 0x3e800000u, 0xbe800000u, 0x80000000u, 0x01000000u, +}; +alignas(256) std::array Output{}; + +struct Constant { + std::uint32_t bits; + std::uint32_t inlineSource; +}; + +constexpr std::array Constants{{ + {0x00000000u, 0x80u}, {0x80000000u, 0xffu}, {0x00000001u, 0xffu}, {0x807fffffu, 0xffu}, {0x3f000000u, 0xf0u}, + {0xc0800000u, 0xf7u}, {0x00800000u, 0xffu}, {0x7f800000u, 0xffu}, {0x7fc00000u, 0xffu}, +}}; + +constexpr std::array Predicates{0x01u, 0x02u, 0x03u, 0x04u, 0x05u, 0x06u, 0x09u, 0x0au, 0x0bu, 0x0cu, 0x0du, 0x0eu}; +constexpr std::array PredicateNames{"lt", "eq", "le", "gt", "lg", "ge", "nge", "nlg", "ngt", "nle", "neq", "nlt"}; + +constexpr std::uint32_t Compares = static_cast(Constants.size() * Predicates.size() * 2u); + +std::vector Build(bool registerOperand) { + std::vector code{0x34020082u, 0x34060085u, 0xe0301000u, 0x80000401u, 0xbf8c3f70u, 0x7e2c0281u}; + for (std::uint32_t word = 0; word < Words; ++word) code.push_back(0x7e000280u | ((30u + word) << 17)); + std::uint32_t bit = 0; + for (const auto& constant : Constants) { + if (registerOperand) code.insert(code.end(), {0x7e2a02ffu, constant.bits}); + const std::uint32_t source = registerOperand ? 256u + 21u : constant.inlineSource; + const bool literal = !registerOperand && source == 0xffu; + for (const std::uint32_t predicate : Predicates) { + for (const bool constantFirst : {true, false}) { + if (constantFirst) { + code.push_back(0x7c000000u | (predicate << 17) | (4u << 9) | source); + } else { + code.push_back(0xd4000000u | (predicate << 16) | 106u); + code.push_back((source << 9) | (256u + 4u)); + } + if (literal) code.push_back(constant.bits); + const std::uint32_t accumulator = 30u + bit / 32u; + code.push_back((0x01u << 25) | (20u << 17) | (22u << 9) | 0x80u); + code.push_back(0xd4000000u | (0x36fu << 16) | accumulator); + code.push_back(((256u + accumulator) << 18) | ((0x80u + bit % 32u) << 9) | (256u + 20u)); + ++bit; + } + } + } + for (std::uint32_t word = 0; word < Words; ++word) code.insert(code.end(), {0xe0701000u | (word * 4u), 0x80010003u | ((30u + word) << 8)}); + code.push_back(0xbf810000u); + return code; +} + +std::uint32_t Flush(std::uint32_t bits, bool flush) { + return flush && (bits & 0x7f800000u) == 0u ? bits & 0x80000000u : bits; +} + +bool Compare(std::uint32_t predicate, std::uint32_t lhsBits, std::uint32_t rhsBits) { + const float lhs = std::bit_cast(lhsBits); + const float rhs = std::bit_cast(rhsBits); + switch (predicate) { + case 0x01u: return lhs < rhs; + case 0x02u: return lhs == rhs; + case 0x03u: return lhs <= rhs; + case 0x04u: return lhs > rhs; + case 0x05u: return lhs < rhs || lhs > rhs; + case 0x06u: return lhs >= rhs; + case 0x09u: return !(lhs >= rhs); + case 0x0au: return !(lhs < rhs || lhs > rhs); + case 0x0bu: return !(lhs > rhs); + case 0x0cu: return !(lhs <= rhs); + case 0x0du: return !(lhs == rhs); + case 0x0eu: return !(lhs < rhs); + default: throw std::runtime_error("f32 compare constant: unknown predicate"); + } +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +void Run(AgcDriver::VulkanDevice& device, const std::optional& floatMode, bool registerOperand) { + Output.fill(0xdeadbeefu); + const auto code = Build(registerOperand); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span words(code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(words)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), words, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = floatMode; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(const std::string& mode, bool flush) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::uint32_t value = Flush(Input[tid], flush); + std::uint32_t bit = 0; + for (const auto& constant : Constants) { + const std::uint32_t flushedConstant = Flush(constant.bits, flush); + for (std::size_t predicate = 0; predicate < Predicates.size(); ++predicate) { + for (const bool constantFirst : {true, false}) { + const bool expected = constantFirst ? Compare(Predicates[predicate], flushedConstant, value) : Compare(Predicates[predicate], value, flushedConstant); + const bool actual = ((Output[tid * Words + bit / 32u] >> (bit % 32u)) & 1u) != 0u; + Require(actual == expected, "f32 compare constant: " + mode + " lane " + std::to_string(tid) + " v_cmp_" + PredicateNames[predicate] + "_f32 " + + (constantFirst ? Hex(constant.bits) + ", " + Hex(Input[tid]) : Hex(Input[tid]) + ", " + Hex(constant.bits)) + " is " + (actual ? "1" : "0") + ", expected " + (expected ? "1" : "0")); + ++bit; + } + } + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + static_assert(Compares <= Words * 32u); + struct Mode { + const char* name; + std::optional floatMode; + bool flush; + }; + const std::array modes{{ + {"no float mode", std::nullopt, true}, + {"IEEE=0 f32 denormals flushed", ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}, true}, + {"IEEE=1 all denormals flushed", ShaderRecompiler::ShaderFloatMode{0x00u, false, true, false}, true}, + }}; + for (const auto& mode : modes) { + for (const bool registerOperand : {true, false}) { + Run(*device, mode.floatMode, registerOperand); + Check(std::string(mode.name) + (registerOperand ? ", register operand" : ", constant operand"), mode.flush); + } + } + std::puts("f32 compare constant tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp index bf5a58d69..34e79fefe 100644 --- a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp +++ b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp @@ -147,6 +147,7 @@ private: void emitIntegerCompare(const RdnaInstruction& inst, IrOpcode opcode, IrType type, bool scalar, bool cmpx); void emitInteger16Compare(const RdnaInstruction& inst, IrOpcode opcode, bool signedValue, bool cmpx); void emitFloatCompare(const RdnaInstruction& inst, IrOpcode opcode, bool half, bool cmpx, bool swap = false); + IrU1 floatCompareWithConstant(IrOpcode opcode, IrU32 variableBits, std::uint32_t constant); void emitInteger64Order(const RdnaInstruction& inst, bool signedValue, bool swap, bool negate, bool cmpx); void emitFloatOrderedCompare(const RdnaInstruction& inst, bool ordered, bool half, bool cmpx); void emitFloatClassCompare(const RdnaInstruction& inst, bool cmpx); diff --git a/core/shader/recompiler/Translation/src/CompareInstructions.cpp b/core/shader/recompiler/Translation/src/CompareInstructions.cpp index 9a8766729..a96ced998 100644 --- a/core/shader/recompiler/Translation/src/CompareInstructions.cpp +++ b/core/shader/recompiler/Translation/src/CompareInstructions.cpp @@ -1,11 +1,47 @@ #include "Translation/CompareInstructions.hpp" #include "Translation/TranslationContext.hpp" #include +#include +#include #include #include namespace ShaderRecompiler { +namespace { + +std::optional constantBits(const IrValue& value) { + if (value.HasImmediate()) return value.ImmediateU32(); + const IrOpcode opcode = value.Opcode(); + if (opcode != IrOpcode::BitwiseAnd32 && opcode != IrOpcode::BitwiseOr32 && opcode != IrOpcode::BitwiseXor32) return std::nullopt; + const auto lhs = constantBits(*value.Argument(0)); + const auto rhs = constantBits(*value.Argument(1)); + if (!lhs.has_value() || !rhs.has_value()) return std::nullopt; + if (opcode == IrOpcode::BitwiseAnd32) return *lhs & *rhs; + if (opcode == IrOpcode::BitwiseOr32) return *lhs | *rhs; + return *lhs ^ *rhs; +} + +IrOpcode mirroredFloatCompare(IrOpcode opcode) { + switch (opcode) { + case IrOpcode::FPOrdLessThan32: return IrOpcode::FPOrdGreaterThan32; + case IrOpcode::FPUnordLessThan32: return IrOpcode::FPUnordGreaterThan32; + case IrOpcode::FPOrdGreaterThan32: return IrOpcode::FPOrdLessThan32; + case IrOpcode::FPUnordGreaterThan32: return IrOpcode::FPUnordLessThan32; + case IrOpcode::FPOrdLessThanEqual32: return IrOpcode::FPOrdGreaterThanEqual32; + case IrOpcode::FPUnordLessThanEqual32: return IrOpcode::FPUnordGreaterThanEqual32; + case IrOpcode::FPOrdGreaterThanEqual32: return IrOpcode::FPOrdLessThanEqual32; + case IrOpcode::FPUnordGreaterThanEqual32: return IrOpcode::FPUnordLessThanEqual32; + case IrOpcode::FPOrdEqual32: + case IrOpcode::FPUnordEqual32: + case IrOpcode::FPOrdNotEqual32: + case IrOpcode::FPUnordNotEqual32: return opcode; + default: throw std::runtime_error("f32 compare against a constant: unexpected compare opcode"); + } +} + +} + void TranslateCompareInstruction(IrBuilder& builder, const RdnaInstruction& instruction) { throw std::runtime_error("TranslateCompareInstruction not implemented"); } @@ -49,13 +85,49 @@ void TranslationContext::emitFloatCompare(const RdnaInstruction& inst, IrOpcode lhs = &readF16AsF32(sourceAt(inst, 0u)).Value(); rhs = &readF16AsF32(sourceAt(inst, 1u)).Value(); } else { - lhs = readOperand(sourceAt(inst, 0u), IrType::F32); - rhs = readOperand(sourceAt(inst, 1u), IrType::F32); + IrValue* lhsBits = readOperand(sourceAt(inst, 0u), IrType::U32); + IrValue* rhsBits = readOperand(sourceAt(inst, 1u), IrType::U32); + if (swap) std::swap(lhsBits, rhsBits); + const auto lhsConstant = constantBits(*lhsBits); + const auto rhsConstant = constantBits(*rhsBits); + if ((f32DenormalFlush & 1u) != 0u && lhsConstant.has_value() != rhsConstant.has_value()) { + const IrOpcode oriented = lhsConstant.has_value() ? mirroredFloatCompare(opcode) : opcode; + emitCompareResult(inst, floatCompareWithConstant(oriented, IrU32(lhsConstant.has_value() ? *rhsBits : *lhsBits), lhsConstant.has_value() ? *lhsConstant : *rhsConstant), false, cmpx); + return; + } + lhs = &ir.BitCastF32(flushF32Denormal(IrU32(*lhsBits)).Value()); + rhs = &ir.BitCastF32(flushF32Denormal(IrU32(*rhsBits)).Value()); + emitCompareResult(inst, IrU1(ir.Emit(opcode, IrType::U1, {lhs, rhs})), false, cmpx); + return; } if (swap) std::swap(lhs, rhs); emitCompareResult(inst, IrU1(ir.Emit(opcode, IrType::U1, {lhs, rhs})), false, cmpx); } +IrU1 TranslationContext::floatCompareWithConstant(IrOpcode opcode, IrU32 variableBits, std::uint32_t constant) { + IrValue& value = ir.BitCastF32(variableBits.Value()); + const auto compare = [&](IrOpcode compareOpcode, IrValue& lhs, std::uint32_t rhs) { return IrU1(ir.Emit(compareOpcode, IrType::U1, {&lhs, &ir.BitCastF32(ir.Constant(rhs))})); }; + if ((constant & 0x7f800000u) != 0u) return compare(opcode, value, constant); + IrValue& magnitude = ir.BitCastF32(ir.BitwiseAnd(variableBits.Value(), ir.Constant(0x7fffffffu))); + constexpr std::uint32_t MinNormal = 0x00800000u; + constexpr std::uint32_t NegativeMinNormal = 0x80800000u; + switch (opcode) { + case IrOpcode::FPOrdLessThan32: return compare(IrOpcode::FPOrdLessThanEqual32, value, NegativeMinNormal); + case IrOpcode::FPUnordLessThan32: return compare(IrOpcode::FPUnordLessThanEqual32, value, NegativeMinNormal); + case IrOpcode::FPOrdLessThanEqual32: return compare(IrOpcode::FPOrdLessThan32, value, MinNormal); + case IrOpcode::FPUnordLessThanEqual32: return compare(IrOpcode::FPUnordLessThan32, value, MinNormal); + case IrOpcode::FPOrdGreaterThan32: return compare(IrOpcode::FPOrdGreaterThanEqual32, value, MinNormal); + case IrOpcode::FPUnordGreaterThan32: return compare(IrOpcode::FPUnordGreaterThanEqual32, value, MinNormal); + case IrOpcode::FPOrdGreaterThanEqual32: return compare(IrOpcode::FPOrdGreaterThan32, value, NegativeMinNormal); + case IrOpcode::FPUnordGreaterThanEqual32: return compare(IrOpcode::FPUnordGreaterThan32, value, NegativeMinNormal); + case IrOpcode::FPOrdEqual32: return compare(IrOpcode::FPOrdLessThan32, magnitude, MinNormal); + case IrOpcode::FPUnordEqual32: return compare(IrOpcode::FPUnordLessThan32, magnitude, MinNormal); + case IrOpcode::FPOrdNotEqual32: return compare(IrOpcode::FPOrdGreaterThanEqual32, magnitude, MinNormal); + case IrOpcode::FPUnordNotEqual32: return compare(IrOpcode::FPUnordGreaterThanEqual32, magnitude, MinNormal); + default: throw std::runtime_error("f32 compare against a constant: unexpected compare opcode"); + } +} + void TranslationContext::emitInteger64Order(const RdnaInstruction& inst, bool signedValue, bool swap, bool negate, bool cmpx) { IrValue* lhs = readOperand(sourceAt(inst, swap ? 1u : 0u), IrType::U64); IrValue* rhs = readOperand(sourceAt(inst, swap ? 0u : 1u), IrType::U64);