mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-08-18 22:42:23 +00:00
467 lines
19 KiB
C++
467 lines
19 KiB
C++
#include "graphics/shader/recompiler/ir/passes/ConstantPropagation.h"
|
|
#include "graphics/shader/recompiler/ir/passes/DeadCodeElimination.h"
|
|
#include "graphics/shader/recompiler/ir/passes/ReadLaneElimination.h"
|
|
#include "graphics/shader/recompiler/ir/passes/SrtWalker.h"
|
|
#include "graphics/shader/recompiler/ir/ValueProgram.h"
|
|
|
|
#include <cstdlib>
|
|
#include <iostream>
|
|
#include <stdexcept>
|
|
#include <unordered_map>
|
|
|
|
namespace {
|
|
|
|
using namespace Libs::Graphics::ShaderRecompiler::IR;
|
|
using Libs::Graphics::ShaderType;
|
|
|
|
void Check(bool condition, const char *message) {
|
|
if (!condition) {
|
|
throw std::runtime_error(message);
|
|
}
|
|
}
|
|
|
|
struct Fixture {
|
|
Program program;
|
|
|
|
explicit Fixture(uint32_t block_count = 1) {
|
|
program.stage = ShaderType::Compute;
|
|
program.shader_hash = 0x12345678u;
|
|
program.user_data_base = 2;
|
|
program.values = std::make_shared<ValueProgram>();
|
|
for (uint32_t index = 0; index < block_count; index++) {
|
|
program.values->block_storage.push_back(std::make_unique<Block>());
|
|
auto *block = program.values->block_storage.back().get();
|
|
program.values->blocks.push_back(block);
|
|
program.values->block_info.push_back({.id = index});
|
|
}
|
|
}
|
|
|
|
Block &BlockAt(uint32_t index = 0) { return *program.values->blocks[index]; }
|
|
|
|
Value Emit(ValueOpcode opcode, std::initializer_list<Value> args = {},
|
|
uint64_t flags = 0, uint32_t block = 0) {
|
|
return Value(&BlockAt(block).AppendNewInst(opcode, args, flags));
|
|
}
|
|
|
|
Value EmitMemory(ValueOpcode opcode, std::initializer_list<Value> args,
|
|
uint32_t memory, uint32_t pc = 0x40, uint32_t block = 0) {
|
|
MemoryFlags flags{.index = memory, .pc = pc};
|
|
uint64_t bits = 0;
|
|
std::memcpy(&bits, &flags, sizeof(flags));
|
|
return Emit(opcode, args, bits, block);
|
|
}
|
|
|
|
uint32_t AddMemory(ResourceKind kind, int32_t offset = 0) {
|
|
MemoryInfo info;
|
|
info.kind = kind;
|
|
info.offset = static_cast<uint32_t>(offset);
|
|
program.values->memory_info.push_back(info);
|
|
return static_cast<uint32_t>(program.values->memory_info.size() - 1u);
|
|
}
|
|
|
|
void Plan() {
|
|
std::string error;
|
|
Check(BuildSrtPlan(program, &error), error.c_str());
|
|
}
|
|
};
|
|
|
|
struct TestMemory {
|
|
std::unordered_map<uint64_t, uint32_t> words;
|
|
uint32_t reads = 0;
|
|
};
|
|
|
|
bool ReadMemory(void *userdata, uint64_t address, uint32_t *value) {
|
|
auto &memory = *static_cast<TestMemory *>(userdata);
|
|
const auto it = memory.words.find(address);
|
|
if (it == memory.words.end()) {
|
|
return false;
|
|
}
|
|
*value = it->second;
|
|
memory.reads++;
|
|
return true;
|
|
}
|
|
|
|
Value Address(Fixture &fixture, Value low, Value high, uint32_t block = 0) {
|
|
return fixture.Emit(ValueOpcode::GetAddressResource, {low, high}, 0, block);
|
|
}
|
|
|
|
Value RawRead(Fixture &fixture, Value address, Value offset, uint32_t memory,
|
|
uint32_t block = 0) {
|
|
return fixture.EmitMemory(ValueOpcode::LoadAddressU32,
|
|
{address, offset, Value(0u), Value(true)}, memory,
|
|
0x80, block);
|
|
}
|
|
|
|
void TestImmediateFlatteningAndGvn() {
|
|
Fixture fixture;
|
|
const auto memory = fixture.AddMemory(ResourceKind::ScalarAddress, 0x20);
|
|
const auto first = RawRead(
|
|
fixture, Address(fixture, Value(0x1000u), Value(0u)), Value(0u), memory);
|
|
const auto second = RawRead(
|
|
fixture, Address(fixture, Value(0x1000u), Value(0u)), Value(0u), memory);
|
|
fixture.Emit(ValueOpcode::GetBufferResource,
|
|
{first, second, Value(16u), Value(0u)});
|
|
|
|
fixture.Plan();
|
|
Check(fixture.program.values->srt_reads.size() == 1,
|
|
"equivalent typed scalar reads were not coalesced");
|
|
Check(fixture.program.values->dynamic_reads.empty(),
|
|
"immediate scalar read was classified as dynamic");
|
|
Check(fixture.program.values->memory_info[memory].planning_only,
|
|
"flattened raw read was not kept as a planning-only root");
|
|
|
|
TestMemory memory_image{{{0x1020u, 0xfeedbeefu}}};
|
|
SrtRuntime runtime{.read_memory = ReadMemory, .userdata = &memory_image};
|
|
std::vector<uint32_t> flat;
|
|
std::string error;
|
|
Check(WalkSrt(fixture.program, runtime, flat, &error), error.c_str());
|
|
Check(flat == std::vector<uint32_t>{0xfeedbeefu} && memory_image.reads == 1,
|
|
"flattened SRT did not evaluate its canonical read once");
|
|
}
|
|
|
|
void TestRawScalarComponentAlignment() {
|
|
Fixture fixture;
|
|
const auto memory = fixture.AddMemory(ResourceKind::ScalarAddress, 2);
|
|
const auto read = RawRead(
|
|
fixture, Address(fixture, Value(0x1003u), Value(0u)), Value(2u), memory);
|
|
fixture.Emit(ValueOpcode::GetBufferResource,
|
|
{read, Value(0u), Value(16u), Value(0u)});
|
|
fixture.Plan();
|
|
|
|
TestMemory memory_image{{{0x1000u, 0x12345678u}}};
|
|
SrtRuntime runtime{.read_memory = ReadMemory, .userdata = &memory_image};
|
|
std::vector<uint32_t> flat;
|
|
std::string error;
|
|
Check(WalkSrt(fixture.program, runtime, flat, &error), error.c_str());
|
|
Check(flat == std::vector<uint32_t>{0x12345678u} &&
|
|
memory_image.reads == 1,
|
|
"raw scalar base, immediate, and offset were not aligned independently");
|
|
}
|
|
|
|
void TestScalarMemoryDomainMismatchFails() {
|
|
Fixture raw;
|
|
const auto raw_memory = raw.AddMemory(ResourceKind::ScalarBuffer);
|
|
RawRead(raw, Address(raw, Value(0x1000u), Value(0u)), Value(0u),
|
|
raw_memory);
|
|
std::string error;
|
|
Check(!BuildSrtPlan(raw.program, &error) &&
|
|
error.find("incompatible scalar memory metadata") !=
|
|
std::string::npos,
|
|
"raw scalar load accepted descriptor-buffer metadata");
|
|
|
|
Fixture buffer;
|
|
const auto buffer_memory = buffer.AddMemory(ResourceKind::ScalarAddress);
|
|
const auto resource = buffer.Emit(ValueOpcode::GetBufferResource,
|
|
{Value(0x1000u), Value(0u), Value(16u),
|
|
Value(0u)});
|
|
buffer.EmitMemory(ValueOpcode::ReadConstBuffer,
|
|
{resource, Value(0u)}, buffer_memory);
|
|
error.clear();
|
|
Check(!BuildSrtPlan(buffer.program, &error) &&
|
|
error.find("incompatible scalar memory metadata") !=
|
|
std::string::npos,
|
|
"descriptor scalar load accepted raw-address metadata");
|
|
}
|
|
|
|
void TestDynamicReadRemainsTyped() {
|
|
Fixture fixture;
|
|
const auto memory = fixture.AddMemory(ResourceKind::ScalarAddress);
|
|
const auto offset = fixture.Emit(ValueOpcode::GetUserData,
|
|
{Value(static_cast<ScalarReg>(2))});
|
|
const auto read = RawRead(
|
|
fixture, Address(fixture, Value(0x1000u), Value(0u)), offset, memory);
|
|
fixture.Emit(ValueOpcode::GetBufferResource,
|
|
{read, Value(0u), Value(16u), Value(0u)});
|
|
|
|
fixture.Plan();
|
|
Check(fixture.program.values->srt_reads.empty() &&
|
|
fixture.program.values->dynamic_reads == std::vector<Value>{read},
|
|
"dynamic scalar read received a fake flattened slot");
|
|
}
|
|
|
|
void TestNestedSrtWalk() {
|
|
Fixture fixture;
|
|
const auto memory = fixture.AddMemory(ResourceKind::ScalarAddress);
|
|
const auto pointer = RawRead(
|
|
fixture, Address(fixture, Value(0x1000u), Value(0u)), Value(0u), memory);
|
|
const auto value =
|
|
RawRead(fixture, Address(fixture, pointer, Value(0u)), Value(0u), memory);
|
|
fixture.Emit(ValueOpcode::GetBufferResource,
|
|
{value, Value(0u), Value(16u), Value(0u)});
|
|
fixture.Plan();
|
|
|
|
TestMemory memory_image{{{0x1000u, 0x2000u}, {0x2000u, 0xabcdef01u}}};
|
|
SrtRuntime runtime{.read_memory = ReadMemory, .userdata = &memory_image};
|
|
std::vector<uint32_t> flat;
|
|
std::string error;
|
|
Check(WalkSrt(fixture.program, runtime, flat, &error), error.c_str());
|
|
Check(flat == std::vector<uint32_t>({0x2000u, 0xabcdef01u}),
|
|
"nested typed SRT reads were not evaluated in dependency order");
|
|
}
|
|
|
|
void TestShaderBaseAndUserData() {
|
|
Fixture fixture;
|
|
const auto base = fixture.Emit(ValueOpcode::GetShaderBase);
|
|
const auto pair = fixture.Emit(ValueOpcode::UnpackUint2x32, {base});
|
|
const auto low =
|
|
fixture.Emit(ValueOpcode::CompositeExtractU32x2, {pair, Value(0u)});
|
|
const auto high =
|
|
fixture.Emit(ValueOpcode::CompositeExtractU32x2, {pair, Value(1u)});
|
|
const auto user = fixture.Emit(ValueOpcode::GetUserData,
|
|
{Value(static_cast<ScalarReg>(2))});
|
|
const auto sum = fixture.Emit(ValueOpcode::IAdd32, {user, Value(4u)});
|
|
fixture.Plan();
|
|
fixture.program.values->descriptor_sources.push_back(
|
|
{.dwords = {low, high, sum}, .dword_count = 3});
|
|
|
|
const std::array user_data{0x20u};
|
|
SrtRuntime runtime{.user_data = user_data,
|
|
.shader_base = 0x12345678abcdef00ull};
|
|
DescriptorValue result;
|
|
std::string error;
|
|
Check(EvaluateDescriptorSource(fixture.program, 0, 0x90, runtime, result,
|
|
&error),
|
|
error.c_str());
|
|
Check(result.dword_count == 3 && result.dwords[0] == 0xabcdef00u &&
|
|
result.dwords[1] == 0x12345678u && result.dwords[2] == 0x24u,
|
|
"shader-relative typed descriptor expression evaluated incorrectly");
|
|
}
|
|
|
|
void TestCarryAndBitFields() {
|
|
Fixture fixture;
|
|
const auto carry =
|
|
fixture.Emit(ValueOpcode::IAddCarry32, {Value(0xffffffffu), Value(2u)});
|
|
const auto low =
|
|
fixture.Emit(ValueOpcode::CompositeExtractU32x2, {carry, Value(0u)});
|
|
const auto high =
|
|
fixture.Emit(ValueOpcode::CompositeExtractU32x2, {carry, Value(1u)});
|
|
const auto inserted =
|
|
fixture.Emit(ValueOpcode::BitFieldInsert,
|
|
{Value(0u), Value(0x89abcdefu), Value(0u), Value(32u)});
|
|
const auto sign = fixture.Emit(ValueOpcode::BitFieldSExtract,
|
|
{Value(0x000000f0u), Value(4u), Value(4u)});
|
|
fixture.Plan();
|
|
fixture.program.values->descriptor_sources.push_back(
|
|
{.dwords = {low, high, inserted, sign}, .dword_count = 4});
|
|
|
|
DescriptorValue result;
|
|
std::string error;
|
|
Check(EvaluateDescriptorSource(fixture.program, 0, 0, {}, result, &error),
|
|
error.c_str());
|
|
Check(result.dwords[0] == 1u && result.dwords[1] == 1u &&
|
|
result.dwords[2] == 0x89abcdefu && result.dwords[3] == 0xffffffffu,
|
|
"typed carry or bit-field runtime evaluation is incorrect");
|
|
}
|
|
|
|
void TestInvariantAndDivergentPhi() {
|
|
Fixture fixture(3);
|
|
auto &invariant = fixture.BlockAt(2).AppendNewInst(ValueOpcode::Phi);
|
|
invariant.SetFlags(Type::U32);
|
|
invariant.AddPhiOperand(&fixture.BlockAt(0), Value(7u));
|
|
invariant.AddPhiOperand(&fixture.BlockAt(1), Value(7u));
|
|
auto &divergent = fixture.BlockAt(2).AppendNewInst(ValueOpcode::Phi);
|
|
divergent.SetFlags(Type::U32);
|
|
divergent.AddPhiOperand(&fixture.BlockAt(0), Value(7u));
|
|
divergent.AddPhiOperand(&fixture.BlockAt(1), Value(9u));
|
|
fixture.Plan();
|
|
fixture.program.values->descriptor_sources.push_back(
|
|
{.dwords = {Value(&invariant)}, .dword_count = 1});
|
|
fixture.program.values->descriptor_sources.push_back(
|
|
{.dwords = {Value(&divergent)}, .dword_count = 1});
|
|
|
|
DescriptorValue result;
|
|
std::string error;
|
|
Check(
|
|
EvaluateDescriptorSource(fixture.program, 0, 0x100, {}, result, &error) &&
|
|
result.dwords[0] == 7u,
|
|
"loop-invariant typed phi was rejected");
|
|
result.dword_count = 4;
|
|
result.dwords[0] = 0xdeadbeefu;
|
|
Check(!EvaluateDescriptorSource(fixture.program, 1, 0x104, {}, result,
|
|
&error) &&
|
|
result.dword_count == 4 && result.dwords[0] == 0xdeadbeefu &&
|
|
error.find("runtime-dependent") != std::string::npos,
|
|
"divergent phi did not fail transactionally");
|
|
}
|
|
|
|
void TestControlDependentStandaloneLoadStaysTyped() {
|
|
Fixture fixture(3);
|
|
const auto memory = fixture.AddMemory(ResourceKind::ScalarAddress);
|
|
auto &base = fixture.BlockAt(2).AppendNewInst(ValueOpcode::Phi);
|
|
base.SetFlags(Type::U32);
|
|
base.AddPhiOperand(&fixture.BlockAt(0), Value(0x1000u));
|
|
base.AddPhiOperand(&fixture.BlockAt(1), Value(0x2000u));
|
|
const auto read =
|
|
RawRead(fixture, Address(fixture, Value(&base), Value(0u), 2), Value(0u),
|
|
memory, 2);
|
|
fixture.Plan();
|
|
Check(fixture.program.values->srt_reads.empty() &&
|
|
read.ResolveInstruction()->GetOpcode() ==
|
|
ValueOpcode::LoadAddressU32 &&
|
|
!fixture.program.values->memory_info[memory].planning_only,
|
|
"control-dependent standalone scalar load was flattened into a host "
|
|
"snapshot");
|
|
}
|
|
|
|
void TestRuntime64BitDescriptorOps() {
|
|
Fixture fixture;
|
|
const auto shifted = fixture.Emit(ValueOpcode::ShiftLeftLogical64,
|
|
{Value(uint64_t{0x1234u}), Value(32u)});
|
|
const auto masked =
|
|
fixture.Emit(ValueOpcode::BitwiseAnd64,
|
|
{shifted, Value(uint64_t{0x0000ffff00000000ull})});
|
|
const auto combined = fixture.Emit(ValueOpcode::BitwiseOr64,
|
|
{masked, Value(uint64_t{0xabcdu})});
|
|
const auto pair = fixture.Emit(ValueOpcode::UnpackUint2x32, {combined});
|
|
const auto low =
|
|
fixture.Emit(ValueOpcode::CompositeExtractU32x2, {pair, Value(0u)});
|
|
const auto high =
|
|
fixture.Emit(ValueOpcode::CompositeExtractU32x2, {pair, Value(1u)});
|
|
fixture.Plan();
|
|
fixture.program.values->descriptor_sources.push_back(
|
|
{.dwords = {low, high}, .dword_count = 2});
|
|
|
|
DescriptorValue result;
|
|
std::string error;
|
|
Check(EvaluateDescriptorSource(fixture.program, 0, 0, {}, result, &error) &&
|
|
result.dwords[0] == 0xabcdu && result.dwords[1] == 0x1234u,
|
|
"64-bit typed descriptor shift/mask evaluation is incorrect");
|
|
}
|
|
|
|
void TestConstantBufferBounds() {
|
|
Fixture fixture;
|
|
const auto memory = fixture.AddMemory(ResourceKind::ScalarBuffer);
|
|
const auto buffer =
|
|
fixture.Emit(ValueOpcode::GetBufferResource,
|
|
{Value(0x3000u), Value(0u), Value(16u), Value(0u)});
|
|
const auto read = fixture.EmitMemory(ValueOpcode::ReadConstBuffer,
|
|
{buffer, Value(12u)}, memory);
|
|
fixture.Emit(ValueOpcode::GetBufferResource,
|
|
{read, Value(0u), Value(16u), Value(0u)});
|
|
fixture.Plan();
|
|
|
|
TestMemory memory_image{{{0x300cu, 0xa5a5a5a5u}}};
|
|
SrtRuntime runtime{.read_memory = ReadMemory, .userdata = &memory_image};
|
|
std::vector<uint32_t> flat;
|
|
std::string error;
|
|
Check(WalkSrt(fixture.program, runtime, flat, &error) &&
|
|
flat == std::vector<uint32_t>{0xa5a5a5a5u},
|
|
error.c_str());
|
|
|
|
Fixture overflow;
|
|
const auto overflow_memory = overflow.AddMemory(ResourceKind::ScalarBuffer);
|
|
const auto overflow_buffer =
|
|
overflow.Emit(ValueOpcode::GetBufferResource,
|
|
{Value(0x3000u), Value(0u), Value(16u), Value(0u)});
|
|
const auto overflow_read =
|
|
overflow.EmitMemory(ValueOpcode::ReadConstBuffer,
|
|
{overflow_buffer, Value(16u)}, overflow_memory);
|
|
overflow.Emit(ValueOpcode::GetBufferResource,
|
|
{overflow_read, Value(0u), Value(16u), Value(0u)});
|
|
overflow.Plan();
|
|
flat = {0x55u};
|
|
Check(!WalkSrt(overflow.program, runtime, flat, &error) &&
|
|
flat == std::vector<uint32_t>{0x55u} &&
|
|
error.find("exceeds size") != std::string::npos,
|
|
"out-of-bounds constant-buffer walk was not transactional");
|
|
}
|
|
|
|
void TestReadLaneElimination() {
|
|
Fixture fixture;
|
|
const auto undef = fixture.Emit(ValueOpcode::UndefU32);
|
|
const auto write = fixture.Emit(ValueOpcode::WriteLane,
|
|
{undef, Value(0xdeadbeefu), Value(5u)});
|
|
const auto read = fixture.Emit(ValueOpcode::ReadLane, {write, Value(5u)});
|
|
const auto use = fixture.Emit(ValueOpcode::IAdd32, {read, Value(1u)});
|
|
const auto stats = EliminateReadLane(*fixture.program.values, 64);
|
|
Check(stats.rewritten_reads == 1 &&
|
|
use.ResolveInstruction()->Arg(0).Resolve() == Value(0xdeadbeefu),
|
|
"fixed-lane typed read was not rewritten from its SSA write chain");
|
|
|
|
const auto selector = fixture.Emit(ValueOpcode::GetUserData,
|
|
{Value(static_cast<ScalarReg>(2))});
|
|
const auto dynamic = fixture.Emit(ValueOpcode::ReadLane, {write, selector});
|
|
fixture.Emit(ValueOpcode::IAdd32, {dynamic, Value(1u)});
|
|
Check(EliminateReadLane(*fixture.program.values, 64).rewritten_reads == 0,
|
|
"dynamic-lane read was rewritten unsafely");
|
|
}
|
|
|
|
void TestOptimizationPipeline() {
|
|
Fixture fixture;
|
|
const auto sum = fixture.Emit(ValueOpcode::IAdd32, {Value(40u), Value(2u)});
|
|
const auto kept = fixture.Emit(ValueOpcode::BitwiseOr32, {sum, Value(0u)});
|
|
fixture.Emit(ValueOpcode::ReferenceU32, {kept});
|
|
fixture.Emit(ValueOpcode::IMul32, {Value(6u), Value(7u)});
|
|
|
|
ConstantPropagationPass(fixture.program.values->blocks);
|
|
RemoveIdentities(fixture.program.values->blocks);
|
|
EliminateDeadCode(fixture.program.values->blocks);
|
|
|
|
const auto &instructions = fixture.BlockAt().Instructions();
|
|
Check(instructions.size() == 1 &&
|
|
instructions.front().GetOpcode() == ValueOpcode::ReferenceU32 &&
|
|
instructions.front().Arg(0).Resolve() == Value(42u),
|
|
"typed constant propagation, identity folding, or dead-code "
|
|
"elimination regressed");
|
|
}
|
|
|
|
void TestUndefinedRuntimeValueFails() {
|
|
Fixture fixture;
|
|
const auto undef = fixture.Emit(ValueOpcode::UndefU32);
|
|
fixture.Plan();
|
|
fixture.program.values->descriptor_sources.push_back(
|
|
{.dwords = {undef}, .dword_count = 1});
|
|
DescriptorValue result;
|
|
result.dword_count = 3;
|
|
std::string error;
|
|
Check(!EvaluateDescriptorSource(fixture.program, 0, 0x200, {}, result,
|
|
&error) &&
|
|
result.dword_count == 3 &&
|
|
error.find("undefined typed runtime value") != std::string::npos,
|
|
"undefined typed descriptor source did not fail transactionally");
|
|
}
|
|
|
|
} // namespace
|
|
|
|
namespace Common {
|
|
|
|
int DbgExitIfHandler(const char *, const char *, int) { return 1; }
|
|
|
|
void DbgExit(int) { std::abort(); }
|
|
|
|
} // namespace Common
|
|
|
|
int main() {
|
|
try {
|
|
TestImmediateFlatteningAndGvn();
|
|
TestRawScalarComponentAlignment();
|
|
TestScalarMemoryDomainMismatchFails();
|
|
TestDynamicReadRemainsTyped();
|
|
TestNestedSrtWalk();
|
|
TestShaderBaseAndUserData();
|
|
TestCarryAndBitFields();
|
|
TestInvariantAndDivergentPhi();
|
|
TestControlDependentStandaloneLoadStaysTyped();
|
|
TestRuntime64BitDescriptorOps();
|
|
TestConstantBufferBounds();
|
|
TestReadLaneElimination();
|
|
TestOptimizationPipeline();
|
|
TestUndefinedRuntimeValueFails();
|
|
std::cout << "TypedValuePlanningTests: all cases passed\n";
|
|
return 0;
|
|
} catch (const std::exception &e) {
|
|
std::cerr << "TypedValuePlanningTests: failed: " << e.what() << '\n';
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
// Keep this focused standalone target self-contained by amalgamating its small
|
|
// typed-IR implementation set.
|
|
#include "../src/graphics/shader/recompiler/ir/Block.cpp"
|
|
#include "../src/graphics/shader/recompiler/ir/passes/ConstantPropagation.cpp"
|
|
#include "../src/graphics/shader/recompiler/ir/passes/DeadCodeElimination.cpp"
|
|
#include "../src/graphics/shader/recompiler/ir/Type.cpp"
|
|
#include "../src/graphics/shader/recompiler/ir/Value.cpp"
|
|
#include "../src/graphics/shader/recompiler/ir/opcodes/ValueOpcodes.cpp"
|
|
#include "../src/graphics/shader/recompiler/ir/ValueProgram.cpp"
|