Files
KytyPS5/tests/ScalarProvenanceTests.cpp
2026-07-16 10:21:41 +02:00

1082 lines
43 KiB
C++

#include "graphics/shader/recompiler/ScalarProvenance.h"
#include "graphics/shader/recompiler/SrtWalker.h"
#include <cstring>
#include <iostream>
#include <stdexcept>
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);
}
}
Operand Sgpr(uint32_t reg) {
Operand operand;
operand.kind = OperandKind::Register;
operand.reg.file = RegisterFile::Scalar;
operand.reg.index = reg;
return operand;
}
Operand Vgpr(uint32_t reg) {
Operand operand;
operand.kind = OperandKind::Register;
operand.reg.file = RegisterFile::Vector;
operand.reg.index = reg;
return operand;
}
Operand Imm(uint32_t value) {
Operand operand;
operand.kind = OperandKind::ImmediateU32;
operand.imm = value;
return operand;
}
Operand M0() {
Operand operand;
operand.kind = OperandKind::Register;
operand.reg.file = RegisterFile::M0;
return operand;
}
Instruction Move(uint32_t pc, uint32_t dst, uint32_t src) {
Instruction inst;
inst.pc = pc;
inst.op = Opcode::MoveU32;
inst.dst = Sgpr(dst);
inst.src[0] = Sgpr(src);
inst.src_count = 1;
return inst;
}
Instruction MoveImmediate(uint32_t pc, uint32_t dst, uint32_t value) {
auto inst = Move(pc, dst, 0);
inst.src[0] = Imm(value);
return inst;
}
Instruction Move64(uint32_t pc, uint32_t dst, uint32_t src) {
auto inst = Move(pc, dst, src);
inst.op = Opcode::MoveU64;
return inst;
}
Instruction WriteLane(uint32_t pc, uint32_t dst, uint32_t src, uint32_t lane) {
Instruction inst;
inst.pc = pc;
inst.op = Opcode::WriteLaneU32;
inst.dst = Vgpr(dst);
inst.src[0] = Sgpr(src);
inst.src[1] = Imm(lane);
inst.src_count = 2;
return inst;
}
Instruction ReadLane(uint32_t pc, uint32_t dst, uint32_t src, uint32_t lane) {
Instruction inst;
inst.pc = pc;
inst.op = Opcode::ReadLaneU32;
inst.dst = Sgpr(dst);
inst.src[0] = Vgpr(src);
inst.src[1] = Imm(lane);
inst.src_count = 2;
return inst;
}
Instruction BufferUse(uint32_t pc, uint32_t base_sgpr) {
Instruction inst;
inst.pc = pc;
inst.op = Opcode::BufferLoadDword;
inst.memory.kind = ResourceKind::Buffer;
inst.memory.resource = base_sgpr / 4;
return inst;
}
const ScalarValue& Value(const Program& program, uint32_t id) {
Check(id < program.provenance.values.size(), "invalid scalar value ID");
return program.provenance.values[id];
}
bool ReadHostMemory(void*, uint64_t address, uint32_t* value) {
if (address == 0 || value == nullptr) {
return false;
}
std::memcpy(value, reinterpret_cast<const void*>(address), sizeof(*value));
return true;
}
struct ReadCounter {
uint32_t reads = 0;
};
bool CountedHostMemory(void* userdata, uint64_t address, uint32_t* value) {
auto* counter = static_cast<ReadCounter*>(userdata);
if (counter != nullptr) {
counter->reads++;
}
return ReadHostMemory(nullptr, address, value);
}
void SetPointer(std::vector<uint32_t>* user_data, uint32_t reg, const void* pointer) {
const auto address = reinterpret_cast<uint64_t>(pointer);
(*user_data)[reg] = static_cast<uint32_t>(address);
(*user_data)[reg + 1u] = static_cast<uint32_t>(address >> 32u);
}
void TestPerUseDescriptorDefinitions() {
Program program;
program.blocks.resize(1);
auto& instructions = program.blocks[0].instructions;
instructions.push_back(BufferUse(0, 0));
for (uint32_t i = 0; i < 4; i++) {
instructions.push_back(Move(4 + i * 4, i, 20 + i));
}
instructions.push_back(BufferUse(20, 0));
std::string error;
Check(BuildScalarProvenance(&program, &error), error.c_str());
const auto* before = GetDescriptorSource(program, instructions[0].memory.resource_source);
const auto* after = GetDescriptorSource(program, instructions[5].memory.resource_source);
Check(before != nullptr && after != nullptr, "descriptor sources were not attached");
Check(*before != *after, "descriptor source was incorrectly shared across SGPR redefinition");
for (uint32_t i = 0; i < 4; i++) {
Check(Value(program, before->dwords[i]).op == ScalarValueOp::UserData &&
Value(program, before->dwords[i]).imm == i,
"pre-copy descriptor does not use the reaching user-data definition");
Check(Value(program, after->dwords[i]).op == ScalarValueOp::UserData &&
Value(program, after->dwords[i]).imm == 20 + i,
"post-copy descriptor does not use the copied user-data definition");
}
}
void TestCfgPhi() {
Program program;
program.blocks.resize(4);
program.blocks[0].successors = {1, 2};
program.blocks[1].predecessors = {0};
program.blocks[1].successors = {3};
program.blocks[2].predecessors = {0};
program.blocks[2].successors = {3};
program.blocks[3].predecessors = {1, 2};
program.blocks[1].instructions.push_back(MoveImmediate(4, 0, 0x11111111));
program.blocks[2].instructions.push_back(MoveImmediate(8, 0, 0x22222222));
program.blocks[3].instructions.push_back(BufferUse(12, 0));
std::string error;
Check(BuildScalarProvenance(&program, &error), error.c_str());
const auto* source =
GetDescriptorSource(program, program.blocks[3].instructions[0].memory.resource_source);
Check(source != nullptr, "merged descriptor source was not attached");
const auto& phi = Value(program, source->dwords[0]);
Check(phi.op == ScalarValueOp::Phi && phi.phi_args.size() == 2,
"differing CFG definitions did not create a provenance phi");
Check(Value(program, phi.phi_args[0]).op == ScalarValueOp::Constant &&
Value(program, phi.phi_args[1]).op == ScalarValueOp::Constant,
"provenance phi does not reference both constant definitions");
Check(BuildSrtPlan(&program, &error) && program.srt.dynamic_sources.size() == 1 &&
program.srt.dynamic_sources[0] ==
program.blocks[3].instructions[0].memory.resource_source,
"acyclic control-flow descriptor phi was not classified dynamic");
}
void TestDiamondReadPathsAreDynamic() {
std::array<uint32_t, 1> left = {0x11111111u};
std::array<uint32_t, 1> right = {0x22222222u};
Program program;
program.blocks.resize(4);
program.blocks[0].successors = {1, 2};
program.blocks[1].predecessors = {0};
program.blocks[1].successors = {3};
program.blocks[2].predecessors = {0};
program.blocks[2].successors = {3};
program.blocks[3].predecessors = {1, 2};
const auto load = [](uint32_t pc, uint32_t base) {
Instruction inst;
inst.pc = pc;
inst.op = Opcode::SLoadDword;
inst.dst = Sgpr(0);
inst.src[0] = Imm(0);
inst.src_count = 1;
inst.memory.kind = ResourceKind::ScalarBuffer;
inst.memory.resource = base;
return inst;
};
program.blocks[1].instructions = {load(4, 4)};
program.blocks[2].instructions = {load(8, 6)};
program.blocks[3].instructions = {BufferUse(12, 0)};
std::string error;
Check(BuildScalarProvenance(&program, &error) && BuildSrtPlan(&program, &error), error.c_str());
const auto source = program.blocks[3].instructions[0].memory.resource_source;
Check(program.srt.reads.empty() && program.srt.dynamic_reads.empty() &&
program.srt.dynamic_sources.size() == 1 && program.srt.dynamic_sources[0] == source,
"inactive diamond read paths were incorrectly flattened");
std::array<uint32_t, 8> user_data = {};
const auto left_address = reinterpret_cast<uint64_t>(left.data());
const auto right_address = reinterpret_cast<uint64_t>(right.data());
user_data[4] = static_cast<uint32_t>(left_address);
user_data[5] = static_cast<uint32_t>(left_address >> 32u);
user_data[6] = static_cast<uint32_t>(right_address);
user_data[7] = static_cast<uint32_t>(right_address >> 32u);
ReadCounter counter;
DescriptorValue descriptor;
const SrtRuntime runtime {user_data, 0, CountedHostMemory, &counter};
Check(!EvaluateDescriptorSource(program, source, 12, runtime, &descriptor, &error) &&
counter.reads == 0 && error.find("GPU-dynamic") != std::string::npos,
"dynamic descriptor evaluation touched an inactive read path");
}
void TestEquivalentConstantPhiIsStatic() {
Program program;
program.blocks.resize(4);
program.blocks[0].successors = {1, 2};
program.blocks[1].predecessors = {0};
program.blocks[1].successors = {3};
program.blocks[2].predecessors = {0};
program.blocks[2].successors = {3};
program.blocks[3].predecessors = {1, 2};
Instruction add;
add.op = Opcode::IAddU32;
add.dst = Sgpr(0);
add.src[0] = Imm(1);
add.src[1] = Imm(1);
add.src_count = 2;
program.blocks[1].instructions = {add};
program.blocks[2].instructions = {MoveImmediate(8, 0, 2)};
program.blocks[3].instructions = {BufferUse(12, 0)};
std::string error;
Check(BuildScalarProvenance(&program, &error) && BuildSrtPlan(&program, &error), error.c_str());
const auto source = program.blocks[3].instructions[0].memory.resource_source;
Check(program.srt.dynamic_sources.empty(),
"equivalent constant phi was unnecessarily classified dynamic");
const std::array<uint32_t, 4> user_data = {};
DescriptorValue descriptor;
const SrtRuntime runtime {user_data, 0, nullptr, nullptr};
Check(EvaluateDescriptorSource(program, source, 12, runtime, &descriptor, &error) &&
descriptor.dwords[0] == 2,
"equivalent constant phi did not evaluate as one static value");
}
void TestWideMoveInvalidatesAndCopiesBothDwords() {
Program program;
program.blocks.resize(1);
auto& instructions = program.blocks[0].instructions;
instructions.push_back(Move64(0, 0, 20));
instructions.push_back(Move64(4, 2, 22));
instructions.push_back(BufferUse(8, 0));
std::string error;
Check(BuildScalarProvenance(&program, &error), error.c_str());
const auto* source = GetDescriptorSource(program, instructions.back().memory.resource_source);
Check(source != nullptr &&
DescriptorSourceResolved(program, instructions.back().memory.resource_source),
"wide descriptor copy did not produce a resolved source");
for (uint32_t i = 0; i < 4; i++) {
Check(Value(program, source->dwords[i]).op == ScalarValueOp::UserData &&
Value(program, source->dwords[i]).imm == 20 + i,
"wide move did not copy both scalar dwords");
}
}
void TestScalarCarryChain() {
Program program;
program.blocks.resize(1);
Instruction low;
low.pc = 0;
low.op = Opcode::ScalarAddCarryU32;
low.dst = Sgpr(4);
low.src[0] = Sgpr(0);
low.src[1] = Imm(16);
low.src[2] = Imm(0);
low.src_count = 3;
Instruction high = low;
high.pc = 4;
high.dst = Sgpr(5);
high.src[0] = Sgpr(1);
high.src[1] = Imm(0);
high.src[2].kind = OperandKind::Register;
high.src[2].reg.file = RegisterFile::Scc;
high.src[2].reg.index = 0;
program.blocks[0].instructions = {low, high, BufferUse(8, 4)};
std::string error;
Check(BuildScalarProvenance(&program, &error), error.c_str());
const auto* source =
GetDescriptorSource(program, program.blocks[0].instructions.back().memory.resource_source);
Check(source != nullptr, "carry-chain descriptor source was not attached");
const auto& low_value = Value(program, source->dwords[0]);
const auto& high_value = Value(program, source->dwords[1]);
Check(low_value.op == ScalarValueOp::AddCarry && high_value.op == ScalarValueOp::AddCarry,
"scalar add/addc pair was not retained");
Check(Value(program, high_value.args[2]).op == ScalarValueOp::Carry,
"high pointer dword does not consume the low add carry");
}
void TestReadConstDefinition() {
Program program;
program.blocks.resize(1);
Instruction load;
load.pc = 4;
load.op = Opcode::SLoadDword;
load.dst = Sgpr(16);
load.src[0] = Imm(8);
load.src_count = 1;
load.memory.kind = ResourceKind::ScalarBuffer;
load.memory.resource = 4;
load.memory.offset = 16;
program.blocks[0].instructions.push_back(load);
program.blocks[0].instructions.push_back(BufferUse(8, 16));
std::string error;
Check(BuildScalarProvenance(&program, &error), error.c_str());
const auto* source =
GetDescriptorSource(program, program.blocks[0].instructions[1].memory.resource_source);
Check(source != nullptr, "read-constant descriptor source was not attached");
const auto& read = Value(program, source->dwords[0]);
Check(read.op == ScalarValueOp::ReadConst && read.imm == 16,
"raw scalar load was not represented as ReadConst");
Check(Value(program, read.args[0]).op == ScalarValueOp::UserData &&
Value(program, read.args[0]).imm == 4 && Value(program, read.args[1]).imm == 5,
"ReadConst pointer does not retain its user-data roots");
Check(Value(program, read.args[2]).op == ScalarValueOp::Constant &&
Value(program, read.args[2]).imm == 8,
"ReadConst dynamic/immediate offset operand was lost");
}
void TestReadConstBufferAndValueNumbering() {
Program program;
program.blocks.resize(1);
Instruction load;
load.op = Opcode::SBufferLoadDword;
load.dst = Sgpr(16);
load.src[0] = Imm(3);
load.src_count = 1;
load.memory.kind = ResourceKind::ScalarBuffer;
load.memory.resource = 1;
load.memory.offset = 12;
program.blocks[0].instructions.push_back(load);
load.pc = 4;
load.dst = Sgpr(17);
program.blocks[0].instructions.push_back(load);
program.blocks[0].instructions.push_back(BufferUse(8, 16));
std::string error;
Check(BuildScalarProvenance(&program, &error), error.c_str());
const auto* source =
GetDescriptorSource(program, program.blocks[0].instructions.back().memory.resource_source);
Check(source != nullptr, "ReadConstBuffer source was not attached");
Check(source->dwords[0] == source->dwords[1],
"identical read-constant values at different PCs were not "
"value-numbered");
const auto& read = Value(program, source->dwords[0]);
Check(read.op == ScalarValueOp::ReadConstBuffer && read.args[3] != 0 && read.args[4] != 0,
"ReadConstBuffer lost descriptor dword 3 or its offset");
}
void TestReadLaneDescriptorSpill() {
std::array<uint32_t, 80> table {};
table[72] = 0x89abcdefu;
Program program;
program.wave_size = 64;
program.user_data_count = 28;
program.blocks.resize(1);
for (uint32_t component = 0; component < 4; component++) {
Instruction load;
load.pc = 0x14;
load.op = Opcode::SBufferLoadDword;
load.dst = Sgpr(84 + component);
load.src[0] = Imm(0);
load.src_count = 1;
load.memory.kind = ResourceKind::ScalarBuffer;
load.memory.resource = 6;
load.memory.offset = 288 + component * 4;
load.memory.component_index = component;
load.memory.component_count = 4;
program.blocks[0].instructions.push_back(load);
}
program.blocks[0].instructions.push_back(WriteLane(0x20, 11, 86, 2));
program.blocks[0].instructions.push_back(WriteLane(0x44, 11, 85, 1));
program.blocks[0].instructions.push_back(WriteLane(0x60, 11, 84, 0));
program.blocks[0].instructions.push_back(MoveImmediate(0xb0, 84, 0));
program.blocks[0].instructions.push_back(ReadLane(0x510, 0, 11, 0));
Instruction sample;
sample.pc = 0x868;
sample.op = Opcode::ImageSample;
sample.memory.kind = ResourceKind::Image;
sample.memory.resource = 0;
sample.memory.sampler = 4;
sample.memory.image_dimension =
Libs::Graphics::ShaderRecompiler::Decoder::ImageDimension::Dim2D;
program.blocks[0].instructions.push_back(sample);
std::string error;
Check(BuildScalarProvenance(&program, &error) && BuildSrtPlan(&program, &error), error.c_str());
const auto source_id = program.blocks[0].instructions.back().memory.resource_source;
const auto* source = GetDescriptorSource(program, source_id);
Check(source != nullptr && DescriptorSourceResolved(program, source_id),
"readlane descriptor spill was not resolved");
Check(Value(program, source->dwords[0]).op == ScalarValueOp::ReadConstBuffer,
"readlane did not recover the pre-overwrite SRT value");
std::vector<uint32_t> user_data(28);
SetPointer(&user_data, 24, table.data());
user_data[26] = sizeof(table);
DescriptorValue descriptor;
const SrtRuntime runtime {user_data, 0, ReadHostMemory, nullptr};
Check(EvaluateDescriptorSource(program, source_id, sample.pc, runtime, &descriptor, &error),
error.c_str());
Check(descriptor.dwords[0] == table[72],
"readlane descriptor spill evaluated the overwritten scalar value");
}
void TestReadLaneVectorOverwriteInvalidatesSpill() {
Program program;
program.wave_size = 64;
program.user_data_count = 8;
program.blocks.resize(1);
Instruction overwrite;
overwrite.pc = 4;
overwrite.op = Opcode::MoveU32;
overwrite.dst = Vgpr(11);
overwrite.src[0] = Imm(0);
overwrite.src_count = 1;
program.blocks[0].instructions = {WriteLane(0, 11, 4, 0), overwrite,
ReadLane(8, 0, 11, 0), BufferUse(12, 0)};
std::string error;
Check(BuildScalarProvenance(&program, &error), error.c_str());
const auto source = program.blocks[0].instructions.back().memory.resource_source;
Check(GetDescriptorSource(program, source) != nullptr &&
!DescriptorSourceResolved(program, source),
"vector overwrite left stale writelane provenance");
}
void TestReadLanePhi() {
Program program;
program.wave_size = 32;
program.user_data_count = 8;
program.blocks.resize(4);
program.blocks[0].successors = {1, 2};
program.blocks[1].predecessors = {0};
program.blocks[1].successors = {3};
program.blocks[2].predecessors = {0};
program.blocks[2].successors = {3};
program.blocks[3].predecessors = {1, 2};
program.blocks[1].instructions = {WriteLane(0, 11, 4, 0)};
program.blocks[2].instructions = {WriteLane(4, 11, 5, 0)};
program.blocks[3].instructions = {ReadLane(8, 0, 11, 0), BufferUse(12, 0)};
std::string error;
Check(BuildScalarProvenance(&program, &error), error.c_str());
const auto* source =
GetDescriptorSource(program, program.blocks[3].instructions.back().memory.resource_source);
Check(source != nullptr && Value(program, source->dwords[0]).op == ScalarValueOp::Phi &&
DescriptorSourceResolved(
program, program.blocks[3].instructions.back().memory.resource_source),
"readlane definitions were not merged across CFG predecessors");
}
void TestReadLaneLoopConvergence() {
Program program;
program.wave_size = 64;
program.user_data_count = 8;
program.blocks.resize(3);
program.blocks[0].successors = {1};
program.blocks[1].predecessors = {0, 2};
program.blocks[1].successors = {2};
program.blocks[2].predecessors = {1};
program.blocks[2].successors = {1};
program.blocks[0].instructions = {WriteLane(0, 11, 4, 0)};
program.blocks[1].instructions = {ReadLane(4, 0, 11, 0), BufferUse(8, 0)};
std::string error;
Check(BuildScalarProvenance(&program, &error), error.c_str());
const auto source = program.blocks[1].instructions.back().memory.resource_source;
Check(DescriptorSourceResolved(program, source),
"unchanged writelane spill did not converge across a loop backedge");
}
void TestReadLaneWideAndRelativeWritesInvalidateSpill() {
const auto check_invalidated = [](Opcode op, const char* message) {
Program program;
program.wave_size = 64;
program.user_data_count = 8;
program.blocks.resize(1);
Instruction overwrite;
overwrite.pc = 4;
overwrite.op = op;
overwrite.dst = Vgpr(11);
overwrite.src[0] = Imm(0);
overwrite.src_count = 1;
program.blocks[0].instructions = {WriteLane(0, 12, 4, 0), overwrite,
ReadLane(8, 0, 12, 0), BufferUse(12, 0)};
std::string error;
Check(BuildScalarProvenance(&program, &error), error.c_str());
const auto source = program.blocks[0].instructions.back().memory.resource_source;
Check(!DescriptorSourceResolved(program, source), message);
};
check_invalidated(Opcode::UMadU64U32,
"64-bit vector destination left stale high-half lane provenance");
check_invalidated(Opcode::MoveRelDestU32,
"relative vector destination left stale lane provenance");
}
void TestReadLaneModuloAndDynamicLane() {
Program modulo;
modulo.wave_size = 32;
modulo.user_data_count = 8;
modulo.blocks.resize(1);
modulo.blocks[0].instructions = {WriteLane(0, 11, 4, 33), ReadLane(4, 0, 11, 65),
BufferUse(8, 0)};
std::string error;
Check(BuildScalarProvenance(&modulo, &error), error.c_str());
const auto* modulo_source =
GetDescriptorSource(modulo, modulo.blocks[0].instructions.back().memory.resource_source);
Check(modulo_source != nullptr &&
Value(modulo, modulo_source->dwords[0]).op == ScalarValueOp::UserData &&
Value(modulo, modulo_source->dwords[0]).imm == 4,
"readlane/writelane did not wrap the lane index to wave size");
Program dynamic;
dynamic.wave_size = 64;
dynamic.user_data_count = 8;
dynamic.blocks.resize(1);
auto write = WriteLane(0, 11, 4, 0);
write.src[1] = Sgpr(7);
dynamic.blocks[0].instructions = {write, ReadLane(4, 0, 11, 0), BufferUse(8, 0)};
Check(BuildScalarProvenance(&dynamic, &error), error.c_str());
Check(!DescriptorSourceResolved(
dynamic, dynamic.blocks[0].instructions.back().memory.resource_source),
"dynamic writelane selector retained unsafe lane provenance");
}
void TestUnresolvedSourceIsMarked() {
Program program;
program.blocks.resize(1);
Instruction unknown;
unknown.op = Opcode::BitwiseNotU64;
unknown.dst = Sgpr(0);
program.blocks[0].instructions = {unknown, BufferUse(4, 0)};
std::string error;
Check(BuildScalarProvenance(&program, &error), error.c_str());
const auto source = program.blocks[0].instructions.back().memory.resource_source;
Check(GetDescriptorSource(program, source) != nullptr &&
!DescriptorSourceResolved(program, source),
"unknown descriptor tuple was incorrectly marked resolved");
}
void TestUnsupportedWideWriteInvalidatesBothDwords() {
Program program;
program.blocks.resize(1);
Instruction unsupported;
unsupported.op = Opcode::BitwiseNotU64;
unsupported.dst = Sgpr(0);
unsupported.src[0] = Sgpr(20);
unsupported.src_count = 1;
program.blocks[0].instructions = {Move(0, 0, 20), Move(4, 1, 21), unsupported,
BufferUse(12, 0)};
std::string error;
Check(BuildScalarProvenance(&program, &error), error.c_str());
const auto* source =
GetDescriptorSource(program, program.blocks[0].instructions.back().memory.resource_source);
Check(source != nullptr && source->dwords[0] == ScalarProvenance::Unknown &&
source->dwords[1] == ScalarProvenance::Unknown,
"unsupported wide write left a stale descriptor half");
}
void TestCyclicPhiIsUnresolved() {
Program program;
program.blocks.resize(2);
program.blocks[0].successors = {1};
program.blocks[1].predecessors = {0, 1};
program.blocks[1].successors = {1};
Instruction increment;
increment.op = Opcode::IAddU32;
increment.dst = Sgpr(0);
increment.src[0] = Sgpr(0);
increment.src[1] = Imm(1);
increment.src_count = 2;
program.blocks[1].instructions = {BufferUse(4, 0), increment};
std::string error;
Check(BuildScalarProvenance(&program, &error), error.c_str());
const auto source = program.blocks[1].instructions[0].memory.resource_source;
Check(GetDescriptorSource(program, source) != nullptr &&
!DescriptorSourceResolved(program, source),
"loop-carried cyclic descriptor provenance was incorrectly resolved");
Check(BuildSrtPlan(&program, &error) && program.srt.dynamic_sources.size() == 1 &&
program.srt.dynamic_sources[0] == source,
"SRT planning did not isolate a cyclic descriptor as a dynamic source");
}
void TestNestedSrtWalk() {
std::array<uint32_t, 4> child = {0x11111111u, 0x22334455u, 0, 0};
std::array<uint32_t, 4> root = {};
const auto child_address = reinterpret_cast<uint64_t>(child.data());
root[0] = static_cast<uint32_t>(child_address);
root[1] = static_cast<uint32_t>(child_address >> 32u);
Program program;
program.blocks.resize(1);
for (uint32_t i = 0; i < 2; i++) {
Instruction load;
load.pc = i * 4;
load.op = Opcode::SLoadDword;
load.dst = Sgpr(8 + i);
load.src[0] = Imm(0);
load.src_count = 1;
load.memory.kind = ResourceKind::ScalarBuffer;
load.memory.resource = 4;
load.memory.offset = i * 4;
program.blocks[0].instructions.push_back(load);
}
Instruction child_load;
child_load.pc = 8;
child_load.op = Opcode::SLoadDword;
child_load.dst = Sgpr(16);
child_load.src[0] = Imm(0);
child_load.src_count = 1;
child_load.memory.kind = ResourceKind::ScalarBuffer;
child_load.memory.resource = 8;
child_load.memory.offset = 6; // RDNA2 SMEM ignores the low two address bits.
program.blocks[0].instructions.push_back(child_load);
program.blocks[0].instructions.push_back(BufferUse(12, 16));
std::string error;
Check(BuildScalarProvenance(&program, &error) && BuildSrtPlan(&program, &error), error.c_str());
Check(program.srt.reads.size() == 3 && program.srt.dynamic_reads.empty(),
"nested SRT reads were not compacted and deduplicated");
std::vector<uint32_t> user_data(64);
SetPointer(&user_data, 4, root.data());
std::vector<uint32_t> flat;
const SrtRuntime runtime {user_data, 0, ReadHostMemory, nullptr};
Check(WalkSrt(program, runtime, &flat, &error), error.c_str());
Check(flat.size() == 3 && flat.back() == child[1],
"nested SRT walker read the wrong aligned dword");
}
void TestDynamicReadIsNotFlattened() {
std::array<uint32_t, 4> table = {0xaabbccddu, 0x12345678u, 0, 0};
Program program;
program.blocks.resize(1);
Instruction load;
load.op = Opcode::SLoadDword;
load.dst = Sgpr(16);
load.src[0] = Sgpr(6);
load.src_count = 1;
load.memory.kind = ResourceKind::ScalarBuffer;
load.memory.resource = 4;
program.blocks[0].instructions = {load, BufferUse(4, 16)};
std::string error;
Check(BuildScalarProvenance(&program, &error) && BuildSrtPlan(&program, &error), error.c_str());
Check(program.srt.reads.empty() && program.srt.dynamic_reads.size() == 1,
"dynamic ReadConst was assigned a flat-buffer offset");
std::vector<uint32_t> user_data(64);
SetPointer(&user_data, 4, table.data());
user_data[6] = 4;
DescriptorValue descriptor;
const auto source = program.blocks[0].instructions[1].memory.resource_source;
const SrtRuntime runtime {user_data, 0, ReadHostMemory, nullptr};
Check(EvaluateDescriptorSource(program, source, 4, runtime, &descriptor, &error),
error.c_str());
Check(descriptor.dwords[0] == table[1], "dynamic ReadConst evaluated the wrong dword");
}
void TestReadConstBufferWalk() {
std::array<uint32_t, 4> table = {0x76543210u, 0, 0, 0};
Program program;
program.blocks.resize(1);
Instruction load;
load.op = Opcode::SBufferLoadDword;
load.dst = Sgpr(16);
load.src[0] = Imm(0);
load.src_count = 1;
load.memory.kind = ResourceKind::ScalarBuffer;
load.memory.resource = 1;
program.blocks[0].instructions = {load, BufferUse(4, 16)};
std::string error;
Check(BuildScalarProvenance(&program, &error) && BuildSrtPlan(&program, &error), error.c_str());
std::vector<uint32_t> user_data(64);
SetPointer(&user_data, 4, table.data());
user_data[6] = 4;
std::vector<uint32_t> flat;
const SrtRuntime runtime {user_data, 0, ReadHostMemory, nullptr};
Check(WalkSrt(program, runtime, &flat, &error), error.c_str());
Check(flat.size() == 1 && flat[0] == table[0], "ReadConstBuffer walker result was wrong");
}
void TestM0ScalarOffset() {
std::array<uint32_t, 2> table = {0x01234567u, 0x89abcdefu};
Program program;
program.blocks.resize(1);
Instruction set_m0;
set_m0.op = Opcode::MoveU32;
set_m0.dst = M0();
set_m0.src[0] = Imm(4);
set_m0.src_count = 1;
Instruction load;
load.pc = 4;
load.op = Opcode::SLoadDword;
load.dst = Sgpr(16);
load.src[0] = M0();
load.src_count = 1;
load.memory.kind = ResourceKind::ScalarBuffer;
load.memory.resource = 4;
program.blocks[0].instructions = {set_m0, load, BufferUse(8, 16)};
std::string error;
Check(BuildScalarProvenance(&program, &error) && BuildSrtPlan(&program, &error), error.c_str());
Check(program.srt.reads.size() == 1 && program.srt.dynamic_reads.empty(),
"constant M0 offset was not flattened");
std::vector<uint32_t> user_data(64);
SetPointer(&user_data, 4, table.data());
std::vector<uint32_t> flat;
const SrtRuntime runtime {user_data, 0, ReadHostMemory, nullptr};
Check(WalkSrt(program, runtime, &flat, &error), error.c_str());
Check(flat.size() == 1 && flat[0] == table[1], "M0 scalar offset evaluated incorrectly");
}
void TestM0ArithmeticPreservesScc() {
Program program;
program.blocks.resize(1);
Instruction low;
low.op = Opcode::ScalarAddCarryU32;
low.dst = M0();
low.src[0] = Sgpr(0);
low.src[1] = Imm(1);
low.src[2] = Imm(0);
low.src_count = 3;
Instruction high = low;
high.pc = 4;
high.dst = Sgpr(16);
high.src[0] = Imm(5);
high.src[1] = Imm(0);
high.src[2].kind = OperandKind::Register;
high.src[2].reg.file = RegisterFile::Scc;
program.blocks[0].instructions = {low,
high,
MoveImmediate(8, 17, 0),
MoveImmediate(12, 18, 0),
MoveImmediate(16, 19, 0),
BufferUse(20, 16)};
std::string error;
Check(BuildScalarProvenance(&program, &error) && BuildSrtPlan(&program, &error), error.c_str());
const std::array<uint32_t, 1> user_data = {0xffffffffu};
const auto source = program.blocks[0].instructions.back().memory.resource_source;
DescriptorValue descriptor;
const SrtRuntime runtime {user_data, 0, nullptr, nullptr};
Check(EvaluateDescriptorSource(program, source, 20, runtime, &descriptor, &error),
error.c_str());
Check(descriptor.dwords[0] == 6, "arithmetic M0 destination lost its SCC carry side effect");
}
void TestShaderRelativePointer() {
std::array<uint32_t, 4> table = {};
Program program;
program.blocks.resize(1);
Instruction getpc_low = MoveImmediate(0x20, 0, 0x24);
getpc_low.src[0].kind = OperandKind::PcRelativeU32;
Instruction getpc_high = MoveImmediate(0x20, 1, 0);
program.blocks[0].instructions = {getpc_low, getpc_high, BufferUse(0x28, 0)};
std::string error;
Check(BuildScalarProvenance(&program, &error) && BuildSrtPlan(&program, &error), error.c_str());
const auto source = program.blocks[0].instructions.back().memory.resource_source;
DescriptorValue descriptor;
const auto shader_base = reinterpret_cast<uint64_t>(table.data()) - 0x24;
const std::array<uint32_t, 4> user_data = {};
const SrtRuntime runtime {user_data, shader_base, nullptr, nullptr};
Check(EvaluateDescriptorSource(program, source, 0x28, runtime, &descriptor, &error),
error.c_str());
const auto expected = reinterpret_cast<uint64_t>(table.data());
Check(descriptor.dwords[0] == static_cast<uint32_t>(expected) &&
descriptor.dwords[1] == static_cast<uint32_t>(expected >> 32u),
"shader-relative pointer lost its high half or carry");
}
void TestSmemComponentAlignment() {
std::array<uint32_t, 2> table = {0x11111111u, 0x22222222u};
std::vector<uint32_t> user_data(20);
SetPointer(&user_data, 4, table.data());
Program raw;
raw.blocks.resize(1);
Instruction raw_load;
raw_load.pc = 4;
raw_load.op = Opcode::SLoadDword;
raw_load.dst = Sgpr(16);
raw_load.src[0] = Imm(2);
raw_load.src_count = 1;
raw_load.memory.kind = ResourceKind::ScalarBuffer;
raw_load.memory.resource = 4;
raw_load.memory.offset = 2;
raw.blocks[0].instructions = {raw_load, BufferUse(12, 16)};
std::string error;
Check(BuildScalarProvenance(&raw, &error) && BuildSrtPlan(&raw, &error), error.c_str());
std::vector<uint32_t> flat;
const SrtRuntime runtime {user_data, 0, ReadHostMemory, nullptr};
Check(WalkSrt(raw, runtime, &flat, &error), error.c_str());
Check(flat.size() == 1 && flat[0] == table[0],
"raw S_LOAD did not clear each address component's low bits");
Program buffer;
buffer.blocks.resize(1);
Instruction buffer_load = raw_load;
buffer_load.op = Opcode::SBufferLoadDword;
buffer_load.memory.resource = 1;
buffer.blocks[0].instructions = {buffer_load, BufferUse(12, 16)};
user_data[6] = sizeof(table);
Check(BuildScalarProvenance(&buffer, &error) && BuildSrtPlan(&buffer, &error), error.c_str());
Check(WalkSrt(buffer, runtime, &flat, &error), error.c_str());
Check(flat.size() == 1 && flat[0] == table[1],
"S_BUFFER_LOAD did not align the summed final address");
buffer.blocks[0].instructions[0].src[0] = Imm(0);
user_data[4] += 2;
Check(BuildScalarProvenance(&buffer, &error) && BuildSrtPlan(&buffer, &error), error.c_str());
Check(WalkSrt(buffer, runtime, &flat, &error), error.c_str());
Check(flat.size() == 1 && flat[0] == table[0],
"S_BUFFER_LOAD did not clear the descriptor base low bits before "
"addition");
}
void TestReadConstSignedAddress() {
std::array<uint32_t, 2> table = {0x11111111u, 0x22222222u};
std::vector<uint32_t> user_data(24);
SetPointer(&user_data, 4, table.data() + 1);
Program program;
program.blocks.resize(1);
Instruction load;
load.pc = 4;
load.op = Opcode::SLoadDword;
load.dst = Sgpr(16);
load.src[0] = Sgpr(20);
load.src_count = 1;
load.memory.kind = ResourceKind::ScalarBuffer;
load.memory.resource = 4;
load.memory.offset = static_cast<uint32_t>(-8);
program.blocks[0].instructions = {MoveImmediate(0, 20, 4), load, BufferUse(8, 16)};
std::string error;
Check(BuildScalarProvenance(&program, &error) && BuildSrtPlan(&program, &error), error.c_str());
std::vector<uint32_t> flat;
const SrtRuntime runtime {user_data, 0, ReadHostMemory, nullptr};
Check(WalkSrt(program, runtime, &flat, &error), error.c_str());
Check(flat.size() == 1 && flat[0] == table[0],
"signed S_LOAD offset resolved the wrong SBASE-relative address");
user_data[4] = 0;
user_data[5] = 0;
Check(!WalkSrt(program, runtime, &flat, &error) &&
error.find("outside the 48-bit address space") != std::string::npos,
"signed S_LOAD address underflow did not fail closed");
}
void TestReadConstBufferBounds() {
std::array<uint32_t, 2> table = {0x11111111u, 0x22222222u};
Program program;
program.stage = ShaderType::Compute;
program.shader_hash = 0x1234;
program.blocks.resize(1);
Instruction load;
load.pc = 0x40;
load.op = Opcode::SBufferLoadDword;
load.dst = Sgpr(16);
load.src[0] = Imm(0);
load.src_count = 1;
load.memory.kind = ResourceKind::ScalarBuffer;
load.memory.resource = 1;
load.memory.offset = sizeof(table);
program.blocks[0].instructions = {load, BufferUse(0x44, 16)};
std::string error;
Check(BuildScalarProvenance(&program, &error) && BuildSrtPlan(&program, &error), error.c_str());
std::vector<uint32_t> user_data(20);
SetPointer(&user_data, 4, table.data());
user_data[5] |= 4u << 16u;
user_data[6] = 2;
ReadCounter counter;
std::vector<uint32_t> flat;
const SrtRuntime runtime {user_data, 0, CountedHostMemory, &counter};
Check(!WalkSrt(program, runtime, &flat, &error),
"out-of-range S_BUFFER_LOAD unexpectedly succeeded");
Check(counter.reads == 0 && error.find("exceeds size=8") != std::string::npos &&
error.find("hash=0x0000000000001234") != std::string::npos &&
error.find("stage=compute") != std::string::npos &&
error.find("pc=0x00000044") != std::string::npos,
"S_BUFFER_LOAD bounds failure read memory or lost shader context");
}
void TestSubBorrowPointerChain() {
Program program;
program.blocks.resize(1);
Instruction low;
low.op = Opcode::ScalarSubBorrowU32;
low.dst = Sgpr(16);
low.src[0] = Sgpr(0);
low.src[1] = Imm(4);
low.src_count = 2;
Instruction high;
high.pc = 4;
high.op = Opcode::ScalarSubBorrowCarryU32;
high.dst = Sgpr(17);
high.src[0] = Sgpr(1);
high.src[1] = Imm(0);
high.src[2].kind = OperandKind::Register;
high.src[2].reg.file = RegisterFile::Scc;
high.src_count = 3;
program.blocks[0].instructions = {low, high, MoveImmediate(8, 18, 0), MoveImmediate(12, 19, 0),
BufferUse(16, 16)};
std::string error;
Check(BuildScalarProvenance(&program, &error) && BuildSrtPlan(&program, &error), error.c_str());
const std::array<uint32_t, 2> user_data = {2, 5};
DescriptorValue descriptor;
const auto source = program.blocks[0].instructions.back().memory.resource_source;
const SrtRuntime runtime {user_data, 0, nullptr, nullptr};
Check(EvaluateDescriptorSource(program, source, 16, runtime, &descriptor, &error),
error.c_str());
Check(descriptor.dwords[0] == 0xfffffffeu && descriptor.dwords[1] == 4,
"scalar subtraction/borrow pointer chain evaluated incorrectly");
}
void TestCommonScalarPointerOps() {
Program program;
program.blocks.resize(1);
const auto binary = [](Opcode op, uint32_t dst, Operand lhs, Operand rhs) {
Instruction inst;
inst.op = op;
inst.dst = Sgpr(dst);
inst.src[0] = lhs;
inst.src[1] = rhs;
inst.src_count = 2;
return inst;
};
Instruction add3;
add3.op = Opcode::IAdd3U32;
add3.dst = Sgpr(19);
add3.src[0] = Sgpr(3);
add3.src[1] = Imm(1);
add3.src[2] = Imm(2);
add3.src_count = 3;
program.blocks[0].instructions = {binary(Opcode::BitwiseXorU32, 16, Sgpr(0), Sgpr(1)),
binary(Opcode::BitwiseAndNotU32, 17, Sgpr(0), Sgpr(1)),
binary(Opcode::IMulU32, 18, Sgpr(2), Imm(4)), add3,
BufferUse(16, 16)};
std::string error;
Check(BuildScalarProvenance(&program, &error) && BuildSrtPlan(&program, &error), error.c_str());
const std::array<uint32_t, 4> user_data = {0xf0f0u, 0x0ff0u, 7u, 9u};
DescriptorValue descriptor;
const auto source = program.blocks[0].instructions.back().memory.resource_source;
const SrtRuntime runtime {user_data, 0, nullptr, nullptr};
Check(EvaluateDescriptorSource(program, source, 16, runtime, &descriptor, &error),
error.c_str());
Check(descriptor.dwords[0] == (user_data[0] ^ user_data[1]) &&
descriptor.dwords[1] == (user_data[0] & ~user_data[1]) &&
descriptor.dwords[2] == 28 && descriptor.dwords[3] == 12,
"common scalar pointer operations evaluated incorrectly");
}
void TestBitFieldMaskDescriptor() {
Program program;
program.blocks.resize(1);
auto mask = MoveImmediate(4, 29, 0);
mask.op = Opcode::BitFieldMaskU32;
mask.src[0] = Imm(12);
mask.src[1] = Imm(12);
mask.src_count = 2;
auto high = MoveImmediate(8, 30, 0x05500000u);
high.op = Opcode::MoveU64;
program.blocks[0].instructions = {MoveImmediate(0, 28, 0x92u), mask, high,
BufferUse(12, 28)};
std::string error;
Check(BuildScalarProvenance(&program, &error) && BuildSrtPlan(&program, &error), error.c_str());
DescriptorValue descriptor;
const SrtRuntime runtime {{}, 0, nullptr, nullptr};
Check(EvaluateDescriptorSource(program,
program.blocks[0].instructions.back().memory.resource_source,
16, runtime, &descriptor, &error),
error.c_str());
Check(descriptor.dwords[0] == 0x92u && descriptor.dwords[1] == 0x00fff000u &&
descriptor.dwords[2] == 0x05500000u && descriptor.dwords[3] == 0,
"production sampler bit-field mask evaluated incorrectly");
mask.src[0] = Imm(0);
program.blocks[0].instructions = {MoveImmediate(0, 28, 0x92u), mask, high,
BufferUse(12, 28)};
Check(BuildScalarProvenance(&program, &error) && BuildSrtPlan(&program, &error), error.c_str());
Check(EvaluateDescriptorSource(program,
program.blocks[0].instructions.back().memory.resource_source,
12, runtime, &descriptor, &error),
error.c_str());
Check(descriptor.dwords[1] == 0, "zero-width bit-field mask was not zero");
mask.src[0] = Imm(31);
mask.src[1] = Imm(31);
program.blocks[0].instructions = {MoveImmediate(0, 28, 0x92u), mask, high,
BufferUse(12, 28)};
Check(BuildScalarProvenance(&program, &error) && BuildSrtPlan(&program, &error), error.c_str());
Check(EvaluateDescriptorSource(program,
program.blocks[0].instructions.back().memory.resource_source,
12, runtime, &descriptor, &error),
error.c_str());
Check(descriptor.dwords[1] == 0x80000000u,
"maximum bit-field mask count/offset evaluated incorrectly");
}
} // namespace
int main() {
try {
TestPerUseDescriptorDefinitions();
TestCfgPhi();
TestDiamondReadPathsAreDynamic();
TestEquivalentConstantPhiIsStatic();
TestWideMoveInvalidatesAndCopiesBothDwords();
TestScalarCarryChain();
TestReadConstDefinition();
TestReadConstBufferAndValueNumbering();
TestReadLaneDescriptorSpill();
TestReadLaneVectorOverwriteInvalidatesSpill();
TestReadLanePhi();
TestReadLaneLoopConvergence();
TestReadLaneWideAndRelativeWritesInvalidateSpill();
TestReadLaneModuloAndDynamicLane();
TestUnresolvedSourceIsMarked();
TestUnsupportedWideWriteInvalidatesBothDwords();
TestCyclicPhiIsUnresolved();
TestNestedSrtWalk();
TestDynamicReadIsNotFlattened();
TestReadConstBufferWalk();
TestM0ScalarOffset();
TestM0ArithmeticPreservesScc();
TestShaderRelativePointer();
TestSmemComponentAlignment();
TestReadConstSignedAddress();
TestReadConstBufferBounds();
TestSubBorrowPointerChain();
TestCommonScalarPointerOps();
TestBitFieldMaskDescriptor();
std::cout << "ScalarProvenanceTests: all cases passed\n";
return 0;
} catch (const std::exception& e) {
std::cerr << "ScalarProvenanceTests: failed: " << e.what() << '\n';
return 1;
}
}