Merge pull request #2408 from karanvora2599/perf/shader-compare-constant-flush

perf(shader): fold the denormal flush into f32 compares against constants
This commit is contained in:
Adrià 2026-10-11 10:14:42 +02:00 committed by GitHub
commit 05c870cc05
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 258 additions and 2 deletions

View File

@ -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 <algorithm>
#include <array>
#include <bit>
#include <cstdint>
#include <cstdio>
#include <iostream>
#include <optional>
#include <stdexcept>
#include <string>
#include <vector>
namespace {
using AgcDriver::Graphics::Require;
using ShaderRecompiler::ShaderStage;
constexpr std::uint32_t Threads = 32;
constexpr std::uint32_t Words = 8;
alignas(256) std::array<std::uint32_t, Threads> 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<std::uint32_t, Threads * Words> Output{};
struct Constant {
std::uint32_t bits;
std::uint32_t inlineSource;
};
constexpr std::array<Constant, 9> Constants{{
{0x00000000u, 0x80u}, {0x80000000u, 0xffu}, {0x00000001u, 0xffu}, {0x807fffffu, 0xffu}, {0x3f000000u, 0xf0u},
{0xc0800000u, 0xf7u}, {0x00800000u, 0xffu}, {0x7f800000u, 0xffu}, {0x7fc00000u, 0xffu},
}};
constexpr std::array<std::uint32_t, 12> Predicates{0x01u, 0x02u, 0x03u, 0x04u, 0x05u, 0x06u, 0x09u, 0x0au, 0x0bu, 0x0cu, 0x0du, 0x0eu};
constexpr std::array<const char*, 12> PredicateNames{"lt", "eq", "le", "gt", "lg", "ge", "nge", "nlg", "ngt", "nle", "neq", "nlt"};
constexpr std::uint32_t Compares = static_cast<std::uint32_t>(Constants.size() * Predicates.size() * 2u);
std::vector<std::uint32_t> Build(bool registerOperand) {
std::vector<std::uint32_t> 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<float>(lhsBits);
const float rhs = std::bit_cast<float>(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<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t bytes) {
const auto address = reinterpret_cast<std::uintptr_t>(data);
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), bytes, 0x31016facu};
}
void Run(AgcDriver::VulkanDevice& device, const std::optional<ShaderRecompiler::ShaderFloatMode>& floatMode, bool registerOperand) {
Output.fill(0xdeadbeefu);
const auto code = Build(registerOperand);
std::vector<std::uint32_t> userData(8, 0u);
const auto input = BufferDescriptor(Input.data(), static_cast<std::uint32_t>(Input.size() * 4u));
const auto output = BufferDescriptor(Output.data(), static_cast<std::uint32_t>(Output.size() * 4u));
std::copy(input.begin(), input.end(), userData.begin());
std::copy(output.begin(), output.end(), userData.begin() + 4);
const std::span<const std::uint32_t> words(code);
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(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<std::uintptr_t>(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<std::uintptr_t>(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<ShaderRecompiler::ShaderFloatMode> floatMode;
bool flush;
};
const std::array<Mode, 3> 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;
}
}

View File

@ -158,6 +158,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);

View File

@ -1,11 +1,47 @@
#include "Translation/CompareInstructions.hpp"
#include "Translation/TranslationContext.hpp"
#include <array>
#include <cstdint>
#include <optional>
#include <stdexcept>
#include <utility>
namespace ShaderRecompiler {
namespace {
std::optional<std::uint32_t> 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);