Files
AnyPS5/core/libs/prx/libSceAgcDriver/tests/RedundantValueElimination.cpp
T
fdfawef324andClaude Opus 5.5 f0f920a66e feat(recompiler): reuse pure IR values recomputed in dominated blocks
The front end rebuilds operand flushes, NaN checks and bitcasts for every
use of a register, and nothing merged them when SPIRV-Tools is off. A
dominator-scoped value numbering pass before SPIR-V emission replaces a
pure value with an earlier identical one in the same or a dominating
block. Only operations that are functions of their operands are merged;
loads, stores, atomics, barriers, phis and cross-lane reads are left
alone. A block the entry does not reach throws.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 22:40:56 -04:00

448 lines
18 KiB
C++

#include "IntermediateRepresentation/IrProgram.hpp"
#include "Optimization/RedundantValueEliminator.hpp"
#include "Recompiler.hpp"
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <exception>
#include <functional>
#include <initializer_list>
#include <set>
#include <span>
#include <spirv/unified1/spirv.hpp>
#include <stdexcept>
#include <string_view>
#include <utility>
#include <vector>
using namespace ShaderRecompiler;
namespace {
struct Builder {
IrProgram program;
IrBlock* block = nullptr;
Builder() {
block = &newBlock();
program.SetEntryBlock(*block);
}
IrBlock& newBlock() {
auto& created = program.CreateBlock();
program.BlockOrder().push_back(&created);
return created;
}
IrValue& constant(std::uint32_t value) {
auto& created = program.CreateValue(IrOpcode::Void, IrType::U32);
created.SetImmediateU32(value);
return created;
}
IrValue& floatConstant(std::uint32_t bits) {
auto& created = program.CreateValue(IrOpcode::Void, IrType::F32);
created.SetImmediateU32(bits);
return created;
}
IrValue& truth() {
auto& created = program.CreateValue(IrOpcode::Void, IrType::Bool);
created.SetImmediateBool(true);
return created;
}
IrValue& emit(IrOpcode opcode, IrType type, std::initializer_list<IrValue*> arguments, std::uint64_t flags = 0u) {
auto& created = program.CreateValue(opcode, type, flags);
for (auto* argument : arguments) created.AddArgument(argument);
block->AppendInstruction(&created);
return created;
}
IrValue& unknown() {
return emit(IrOpcode::LaneId, IrType::U32, {});
}
IrValue& flush(IrValue& bits) {
auto& exponent = emit(IrOpcode::BitwiseAnd32, IrType::U32, {&bits, &constant(0x7f800000u)});
auto& denormal = emit(IrOpcode::IEqual32, IrType::Bool, {&exponent, &constant(0u)});
auto& sign = emit(IrOpcode::BitwiseAnd32, IrType::U32, {&bits, &constant(0x80000000u)});
return emit(IrOpcode::SelectU32, IrType::U32, {&denormal, &sign, &bits});
}
void keep(IrValue& value) {
static_cast<void>(emit(IrOpcode::ReferenceU32, IrType::Void, {&value}));
}
std::uint32_t eliminate() {
return RedundantValueEliminator{}.Eliminate(program).replacedValues;
}
};
bool expect(const char* name, bool condition) {
if (!condition) std::fprintf(stderr, "%s\n", name);
return condition;
}
bool run(const char* name, const std::function<bool()>& test) {
try {
return expect(name, test());
} catch (const std::exception& error) {
std::fprintf(stderr, "%s: %s\n", name, error.what());
return false;
}
}
bool AllUsed(const std::vector<IrValue*>& values) {
for (const auto* value : values) {
if (!value->HasUses()) return false;
}
return true;
}
bool PureSpirv(std::uint32_t opcode) {
switch (opcode) {
case spv::OpBitcast:
case spv::OpUConvert:
case spv::OpSConvert:
case spv::OpFConvert:
case spv::OpConvertFToU:
case spv::OpConvertFToS:
case spv::OpConvertUToF:
case spv::OpConvertSToF:
case spv::OpCompositeConstruct:
case spv::OpCompositeExtract:
case spv::OpSNegate:
case spv::OpFNegate:
case spv::OpIAdd:
case spv::OpISub:
case spv::OpIMul:
case spv::OpFAdd:
case spv::OpFSub:
case spv::OpFMul:
case spv::OpShiftLeftLogical:
case spv::OpShiftRightLogical:
case spv::OpShiftRightArithmetic:
case spv::OpBitwiseAnd:
case spv::OpBitwiseOr:
case spv::OpBitwiseXor:
case spv::OpNot:
case spv::OpBitFieldInsert:
case spv::OpBitFieldUExtract:
case spv::OpBitFieldSExtract:
case spv::OpBitReverse:
case spv::OpBitCount:
case spv::OpSelect:
case spv::OpIEqual:
case spv::OpINotEqual:
case spv::OpULessThan:
case spv::OpULessThanEqual:
case spv::OpUGreaterThan:
case spv::OpUGreaterThanEqual:
case spv::OpSLessThan:
case spv::OpSLessThanEqual:
case spv::OpSGreaterThan:
case spv::OpSGreaterThanEqual:
case spv::OpFOrdEqual:
case spv::OpFOrdNotEqual:
case spv::OpFOrdLessThan:
case spv::OpFOrdGreaterThan:
case spv::OpFOrdLessThanEqual:
case spv::OpFOrdGreaterThanEqual:
case spv::OpFUnordEqual:
case spv::OpFUnordNotEqual:
case spv::OpFUnordLessThan:
case spv::OpFUnordGreaterThan:
case spv::OpFUnordLessThanEqual:
case spv::OpFUnordGreaterThanEqual:
case spv::OpIsNan:
case spv::OpLogicalAnd:
case spv::OpLogicalOr:
case spv::OpLogicalNot:
case spv::OpLogicalEqual:
case spv::OpLogicalNotEqual:
return true;
default:
return false;
}
}
std::size_t RepeatedInBlock(std::span<const std::uint32_t> words) {
if (words.size() < 5 || words[0] != spv::MagicNumber) throw std::runtime_error("the recompiler returned no SPIR-V module");
std::size_t repeated = 0;
std::set<std::vector<std::uint32_t>> seen;
for (std::size_t offset = 5; offset < words.size();) {
const auto count = words[offset] >> spv::WordCountShift;
if (count == 0 || count > words.size() - offset) throw std::runtime_error("the recompiler returned a malformed SPIR-V instruction");
const auto opcode = words[offset] & spv::OpCodeMask;
if (opcode == spv::OpLabel) seen.clear();
if (PureSpirv(opcode) && count >= 3) {
std::vector<std::uint32_t> key{opcode, words[offset + 1]};
key.insert(key.end(), words.begin() + static_cast<std::ptrdiff_t>(offset + 3), words.begin() + static_cast<std::ptrdiff_t>(offset + count));
if (!seen.insert(std::move(key)).second) ++repeated;
}
offset += count;
}
return repeated;
}
std::vector<std::uint32_t> Recompiled(std::span<const std::uint32_t> code) {
const std::array<std::uint32_t, 4> userData{0x10000000u, 0x00100000u, 0x40u, 0x00027facu};
const std::array<std::uint32_t, 2> capabilities{1u, 61u};
const std::array<std::string_view, 1> extensions{"SPV_KHR_storage_buffer_storage_class"};
RecompileRequest request{};
request.shader = {ShaderStage::Compute, 0x20000u, code, 0, {}};
request.context.waveSize = 64;
request.context.userDataBaseRegister = 0;
request.context.userData = userData;
request.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 0u, {false, false, false}, false, 1u};
request.target.vulkanVersion = 0x00403000u;
request.target.spirvVersion = 0x00010600u;
request.target.subgroupSize = 32u;
request.target.bdaAbiVersion = 1;
request.target.supportedCapabilities = capabilities;
request.target.supportedExtensions = extensions;
request.target.maxWorkgroupSize = {1024u, 1024u, 64u};
request.target.maxWorkgroupInvocations = 1024;
request.target.maxWorkgroupSharedMemoryBytes = 49152;
request.layout = {0, 0, 0, 128};
request.useCache = false;
const auto& words = Recompile(request).spirv.Words();
return {words.begin(), words.end()};
}
}
int main() {
bool passed = true;
passed &= run("a value recomputed from the same operands in one block is replaced by the first", [] {
Builder b;
auto& source = b.unknown();
auto& first = b.flush(source);
auto& second = b.flush(source);
b.keep(first);
b.keep(second);
return b.eliminate() == 4u && !second.HasUses() && first.UseCount() == 2u;
});
passed &= run("values that differ in an immediate, an operand or the opcode stay", [] {
Builder b;
auto& source = b.unknown();
auto& other = b.unknown();
auto& masked = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&source, &b.constant(0x7f800000u)});
auto& otherMask = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&source, &b.constant(0x80000000u)});
auto& otherSource = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&other, &b.constant(0x7f800000u)});
auto& otherOpcode = b.emit(IrOpcode::BitwiseOr32, IrType::U32, {&source, &b.constant(0x7f800000u)});
b.keep(masked);
b.keep(otherMask);
b.keep(otherSource);
b.keep(otherOpcode);
return b.eliminate() == 0u && AllUsed({&masked, &otherMask, &otherSource, &otherOpcode});
});
passed &= run("values that differ only in flags, the result type or the type of an immediate stay", [] {
Builder b;
auto& source = b.unknown();
auto& plain = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&source, &b.constant(0x3f800000u)});
auto& flagged = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&source, &b.constant(0x3f800000u)}, 1u);
auto& otherType = b.emit(IrOpcode::BitwiseAnd32, IrType::F32, {&source, &b.constant(0x3f800000u)});
auto& floatImmediate = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&source, &b.floatConstant(0x3f800000u)});
b.keep(plain);
b.keep(flagged);
b.keep(otherType);
b.keep(floatImmediate);
return b.eliminate() == 0u && AllUsed({&plain, &flagged, &otherType, &floatImmediate});
});
passed &= run("float immediates compare by bits: +0 and -0 and two NaN payloads stay apart, one NaN merges", [] {
Builder b;
auto& x = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&b.unknown()});
auto& plusZero = b.emit(IrOpcode::FPAdd32, IrType::F32, {&x, &b.floatConstant(0x00000000u)});
auto& minusZero = b.emit(IrOpcode::FPAdd32, IrType::F32, {&x, &b.floatConstant(0x80000000u)});
auto& quiet = b.emit(IrOpcode::FPMax32, IrType::F32, {&x, &b.floatConstant(0x7fc00000u)});
auto& sameQuiet = b.emit(IrOpcode::FPMax32, IrType::F32, {&x, &b.floatConstant(0x7fc00000u)});
auto& otherPayload = b.emit(IrOpcode::FPMax32, IrType::F32, {&x, &b.floatConstant(0x7fc00001u)});
for (auto* value : {&plusZero, &minusZero, &quiet, &sameQuiet, &otherPayload}) b.keep(b.emit(IrOpcode::BitCastU32F32, IrType::U32, {value}));
return b.eliminate() == 2u && !sameQuiet.HasUses() && AllUsed({&quiet, &plusZero, &minusZero, &otherPayload});
});
passed &= run("a value recomputed in a dominated block is replaced, one in a sibling block stays", [] {
Builder b;
IrBlock& head = *b.block;
IrBlock& then = b.newBlock();
IrBlock& otherwise = b.newBlock();
IrBlock& merge = b.newBlock();
head.AddBranch(&then);
head.AddBranch(&otherwise);
then.AddBranch(&merge);
otherwise.AddBranch(&merge);
auto& source = b.unknown();
auto& other = b.unknown();
auto& first = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&source});
b.keep(first);
b.block = &then;
auto& dominated = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&source});
auto& thenOnly = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&other});
b.keep(dominated);
b.keep(thenOnly);
b.block = &otherwise;
auto& sibling = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&other});
b.keep(sibling);
b.block = &merge;
auto& merged = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&other});
b.keep(merged);
return b.eliminate() == 1u && !dominated.HasUses() && first.UseCount() == 2u && AllUsed({&thenOnly, &sibling, &merged});
});
passed &= run("a loop body reuses a value of its header, the loop exit does not reuse a value of the body", [] {
Builder b;
IrBlock& header = b.newBlock();
IrBlock& body = b.newBlock();
IrBlock& exit = b.newBlock();
b.block->AddBranch(&header);
header.AddBranch(&body);
header.AddBranch(&exit);
body.AddBranch(&header);
auto& source = b.unknown();
auto& other = b.unknown();
b.block = &header;
auto& inHeader = b.emit(IrOpcode::IAdd32, IrType::U32, {&source, &b.constant(1u)});
b.keep(inHeader);
b.block = &body;
auto& inBody = b.emit(IrOpcode::IAdd32, IrType::U32, {&source, &b.constant(1u)});
auto& bodyOnly = b.emit(IrOpcode::IAdd32, IrType::U32, {&other, &b.constant(1u)});
b.keep(inBody);
b.keep(bodyOnly);
b.block = &exit;
auto& afterLoop = b.emit(IrOpcode::IAdd32, IrType::U32, {&other, &b.constant(1u)});
b.keep(afterLoop);
return b.eliminate() == 1u && !inBody.HasUses() && inHeader.UseCount() == 2u && AllUsed({&bodyOnly, &afterLoop});
});
passed &= run("a branch condition that is replaced names the value that stays", [] {
Builder b;
auto& source = b.unknown();
auto& first = b.emit(IrOpcode::IEqual32, IrType::Bool, {&source, &b.constant(0u)});
auto& second = b.emit(IrOpcode::IEqual32, IrType::Bool, {&source, &b.constant(0u)});
b.keep(b.emit(IrOpcode::SelectU32, IrType::U32, {&first, &source, &b.constant(1u)}));
b.keep(b.emit(IrOpcode::SelectU32, IrType::U32, {&second, &b.constant(1u), &source}));
BlockInfo info;
info.condition = &second;
b.program.Metadata().blockInfo.push_back(info);
return b.eliminate() == 1u && b.program.Metadata().blockInfo.front().condition == &first;
});
passed &= run("lane reads are not merged", [] {
Builder b;
auto& first = b.unknown();
auto& second = b.unknown();
b.keep(first);
b.keep(second);
return b.eliminate() == 0u && second.HasUses();
});
passed &= run("cross-lane reads with the same operands are not merged", [] {
Builder b;
auto& source = b.unknown();
std::vector<IrValue*> values;
for (int copy = 0; copy < 2; ++copy) {
values.push_back(&b.emit(IrOpcode::ReadFirstLane, IrType::U32, {&source, &b.truth()}));
values.push_back(&b.emit(IrOpcode::ReadLane, IrType::U32, {&source, &b.constant(3u)}));
values.push_back(&b.emit(IrOpcode::DppMoveU32, IrType::U32, {&source, &b.truth()}));
values.push_back(&b.emit(IrOpcode::SwizzleU32, IrType::U32, {&source, &b.constant(0x1fu), &b.truth()}));
}
for (auto* value : values) b.keep(*value);
return b.eliminate() == 0u && AllUsed(values);
});
passed &= run("loads of one address are not merged, with no store, a store or a barrier between them", [] {
Builder b;
auto& address = b.unknown();
std::vector<IrValue*> loads;
loads.push_back(&b.emit(IrOpcode::LoadShared, IrType::U32, {&address}));
loads.push_back(&b.emit(IrOpcode::LoadShared, IrType::U32, {&address}));
static_cast<void>(b.emit(IrOpcode::StoreShared, IrType::Void, {&address, &b.constant(7u)}));
loads.push_back(&b.emit(IrOpcode::LoadShared, IrType::U32, {&address}));
static_cast<void>(b.emit(IrOpcode::Barrier, IrType::Void, {}));
loads.push_back(&b.emit(IrOpcode::LoadShared, IrType::U32, {&address}));
for (auto* load : loads) b.keep(*load);
return b.eliminate() == 0u && AllUsed(loads);
});
passed &= run("atomics with the same operands are not merged", [] {
Builder b;
auto& address = b.unknown();
auto& first = b.emit(IrOpcode::SharedAtomicIAdd32, IrType::U32, {&address, &b.constant(1u), &b.truth()});
auto& second = b.emit(IrOpcode::SharedAtomicIAdd32, IrType::U32, {&address, &b.constant(1u), &b.truth()});
b.keep(first);
b.keep(second);
return b.eliminate() == 0u && AllUsed({&first, &second});
});
passed &= run("phis with the same incoming values are not merged", [] {
Builder b;
IrBlock& left = b.newBlock();
IrBlock& right = b.newBlock();
IrBlock& merge = b.newBlock();
b.block->AddBranch(&left);
b.block->AddBranch(&right);
left.AddBranch(&merge);
right.AddBranch(&merge);
auto& one = b.unknown();
auto& two = b.unknown();
b.block = &merge;
auto& first = b.emit(IrOpcode::Phi, IrType::U32, {});
auto& second = b.emit(IrOpcode::Phi, IrType::U32, {});
for (auto* phi : {&first, &second}) {
phi->AddPhiOperand(&left, &one);
phi->AddPhiOperand(&right, &two);
}
b.keep(first);
b.keep(second);
return b.eliminate() == 0u && AllUsed({&first, &second});
});
passed &= run("float operations with the same operands in the same order are merged", [] {
Builder b;
auto& lhs = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&b.unknown()});
auto& rhs = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&b.unknown()});
auto& first = b.emit(IrOpcode::FPMul32, IrType::F32, {&lhs, &rhs});
auto& second = b.emit(IrOpcode::FPMul32, IrType::F32, {&lhs, &rhs});
auto& swapped = b.emit(IrOpcode::FPMul32, IrType::F32, {&rhs, &lhs});
b.keep(b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&first}));
b.keep(b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&second}));
b.keep(b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&swapped}));
return b.eliminate() == 2u && !second.HasUses() && swapped.HasUses();
});
passed &= run("a block that the entry does not reach throws", [] {
Builder b;
static_cast<void>(b.newBlock());
try {
static_cast<void>(b.eliminate());
} catch (const std::runtime_error&) {
return true;
}
return false;
});
passed &= run("a wave64 shader that reads one register in several float operations computes nothing twice in a block", [] {
const std::array<std::uint32_t, 7> code{
0x10020100u,
0x10040300u,
0x10060500u,
0x06020503u,
0xe0700000u, 0x80000100u,
0xbf810000u,
};
const auto module = Recompiled(code);
const auto repeated = RepeatedInBlock(module);
if (repeated != 0u) std::fprintf(stderr, " %zu pure instructions repeat an earlier one in their block of the %zu-word module\n", repeated, module.size());
return repeated == 0u;
});
return passed ? 0 : 1;
}