mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-08-19 06:52:36 +00:00
699 lines
28 KiB
C++
699 lines
28 KiB
C++
#include "graphics/guest_gpu/gpu_defs.h"
|
|
#include "graphics/shader/recompiler/ir/passes/BindingLayout.h"
|
|
#include "graphics/shader/recompiler/ir/passes/ResourceMaterialization.h"
|
|
#include "graphics/shader/recompiler/ir/passes/ResourceTracking.h"
|
|
#include "graphics/shader/recompiler/ir/passes/ShaderInfoCollection.h"
|
|
#include "graphics/shader/recompiler/ir/passes/SrtWalker.h"
|
|
#include "graphics/shader/recompiler/ir/ValueProgram.h"
|
|
|
|
#include <array>
|
|
#include <cstring>
|
|
#include <iostream>
|
|
#include <limits>
|
|
#include <memory>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace {
|
|
|
|
using namespace Libs::Graphics::ShaderRecompiler::IR;
|
|
using Libs::Graphics::ShaderComputeInputInfo;
|
|
using Libs::Graphics::ShaderType;
|
|
namespace Decoder = Libs::Graphics::ShaderRecompiler::Decoder;
|
|
|
|
void Check(bool condition, const char *message) {
|
|
if (!condition) {
|
|
throw std::runtime_error(message);
|
|
}
|
|
}
|
|
|
|
struct Fixture {
|
|
Program program;
|
|
Block *block = nullptr;
|
|
|
|
explicit Fixture(ShaderType stage = ShaderType::Compute) {
|
|
program.stage = stage;
|
|
program.values = std::make_shared<ValueProgram>();
|
|
program.user_data_count = 64;
|
|
block = AddBlock();
|
|
}
|
|
|
|
Block *AddBlock() {
|
|
auto storage = std::make_unique<Block>();
|
|
auto *result = storage.get();
|
|
program.values->block_storage.push_back(std::move(storage));
|
|
program.values->blocks.push_back(result);
|
|
program.values->block_info.push_back(
|
|
{.id = static_cast<uint32_t>(program.values->block_info.size())});
|
|
return result;
|
|
}
|
|
|
|
Value Emit(ValueOpcode opcode, std::initializer_list<Value> args = {},
|
|
uint64_t flags = 0, Block *destination = nullptr) {
|
|
if (NumArgsOf(opcode) != std::numeric_limits<size_t>::max() &&
|
|
NumArgsOf(opcode) != args.size()) {
|
|
throw std::runtime_error(std::string(ValueOpcodeName(opcode)) +
|
|
" argument count");
|
|
}
|
|
auto &inst = (destination != nullptr ? destination : block)
|
|
->AppendNewInst(opcode, args, flags);
|
|
return Value(&inst);
|
|
}
|
|
|
|
template <typename T>
|
|
Value Emit(ValueOpcode opcode, std::initializer_list<Value> args, T flags,
|
|
Block *destination = nullptr) {
|
|
uint64_t bits = 0;
|
|
std::memcpy(&bits, &flags, sizeof(flags));
|
|
return Emit(opcode, args, bits, destination);
|
|
}
|
|
|
|
Value UserData(uint32_t index) {
|
|
return Emit(ValueOpcode::GetUserData,
|
|
{Value(static_cast<ScalarReg>(index))});
|
|
}
|
|
|
|
MemoryFlags AddMemory(MemoryInfo memory, uint32_t pc) {
|
|
const auto index =
|
|
static_cast<uint32_t>(program.values->memory_info.size());
|
|
program.values->memory_info.push_back(memory);
|
|
return {index, pc};
|
|
}
|
|
|
|
Value Buffer(std::array<Value, 4> dwords, uint32_t pc = 0) {
|
|
return Emit(ValueOpcode::GetBufferResource,
|
|
{dwords[0], dwords[1], dwords[2], dwords[3]},
|
|
MemoryFlags{0, pc});
|
|
}
|
|
|
|
Value Address(Value low, Value high, uint32_t pc = 0) {
|
|
return Emit(ValueOpcode::GetAddressResource, {low, high},
|
|
MemoryFlags{0, pc});
|
|
}
|
|
|
|
Value Image(std::array<Value, 8> dwords, uint32_t pc = 0) {
|
|
return Emit(ValueOpcode::GetImageResource,
|
|
{dwords[0], dwords[1], dwords[2], dwords[3], dwords[4],
|
|
dwords[5], dwords[6], dwords[7]},
|
|
MemoryFlags{0, pc});
|
|
}
|
|
|
|
Value Sampler(std::array<Value, 4> dwords, uint32_t pc = 0) {
|
|
return Emit(ValueOpcode::GetSamplerResource,
|
|
{dwords[0], dwords[1], dwords[2], dwords[3]},
|
|
MemoryFlags{0, pc});
|
|
}
|
|
|
|
Value ImageAddress() {
|
|
return Emit(ValueOpcode::MakeImageAddress,
|
|
{Value(0u), Value(0u), Value(0u), Value(0u), Value(0u),
|
|
Value(0u), Value(0u), Value(0u), Value(0u), Value(0u),
|
|
Value(0u), Value(0u), Value(0u)});
|
|
}
|
|
|
|
void PlanAndTrack() {
|
|
std::string error;
|
|
if (!BuildSrtPlan(program, &error) || !TrackResources(program, &error)) {
|
|
throw std::runtime_error(error);
|
|
}
|
|
}
|
|
};
|
|
|
|
struct TestMemory {
|
|
uint64_t base = 0x1000;
|
|
std::array<uint32_t, 8> words{};
|
|
uint32_t reads = 0;
|
|
uint32_t fail_after = UINT32_MAX;
|
|
};
|
|
|
|
bool ReadTestMemory(void *userdata, uint64_t address, uint32_t *value) {
|
|
auto *memory = static_cast<TestMemory *>(userdata);
|
|
if (memory == nullptr || value == nullptr || address < memory->base ||
|
|
address - memory->base >= memory->words.size() * sizeof(uint32_t) ||
|
|
memory->reads >= memory->fail_after) {
|
|
return false;
|
|
}
|
|
*value = memory->words[(address - memory->base) / sizeof(uint32_t)];
|
|
memory->reads++;
|
|
return true;
|
|
}
|
|
|
|
void TestDenseBufferTracking() {
|
|
Fixture fixture;
|
|
std::array<Value, 8> userdata;
|
|
for (uint32_t index = 0; index < userdata.size(); index++) {
|
|
userdata[index] = fixture.UserData(index);
|
|
}
|
|
const auto first =
|
|
fixture.Buffer({userdata[0], userdata[1], userdata[2], userdata[3]}, 4);
|
|
const auto second =
|
|
fixture.Buffer({userdata[4], userdata[5], userdata[6], userdata[7]}, 28);
|
|
|
|
MemoryInfo load_info;
|
|
load_info.kind = ResourceKind::Buffer;
|
|
load_info.offset = 4;
|
|
load_info.formatted = true;
|
|
const auto load_flags = fixture.AddMemory(load_info, 4);
|
|
fixture.Emit(ValueOpcode::LoadBufferU32,
|
|
{first, Value(0u), Value(0u), Value(0u), Value(true)},
|
|
load_flags);
|
|
|
|
auto store_info = load_info;
|
|
store_info.offset = 12;
|
|
const auto store_flags = fixture.AddMemory(store_info, 8);
|
|
fixture.Emit(ValueOpcode::StoreBufferU32,
|
|
{first, Value(0u), Value(0u), Value(0u), Value(7u), Value(true)},
|
|
store_flags);
|
|
|
|
auto atomic_info = load_info;
|
|
atomic_info.offset = 0;
|
|
const auto atomic_flags = fixture.AddMemory(atomic_info, 12);
|
|
fixture.Emit(ValueOpcode::BufferAtomicIAdd32,
|
|
{first, Value(0u), Value(0u), Value(1u), Value(0u), Value(true)},
|
|
atomic_flags);
|
|
|
|
const auto other_flags = fixture.AddMemory(load_info, 28);
|
|
fixture.Emit(ValueOpcode::LoadBufferU32,
|
|
{second, Value(0u), Value(0u), Value(0u), Value(true)},
|
|
other_flags);
|
|
fixture.PlanAndTrack();
|
|
|
|
Check(fixture.program.info.buffers.size() == 2,
|
|
"typed buffer sources were not densely interned");
|
|
Check(fixture.program.values->descriptor_sources.size() == 2,
|
|
"descriptor source table did not match dense topology");
|
|
const auto &resource = fixture.program.info.buffers[0];
|
|
Check(resource.read && resource.written && resource.atomic &&
|
|
resource.formatted && resource.max_byte_extent == 16 &&
|
|
resource.first_use_pc == 4,
|
|
"buffer access facts were not merged");
|
|
Check(first.Instruction()->Flags<uint32_t>() == 0 &&
|
|
second.Instruction()->Flags<uint32_t>() == 1,
|
|
"typed handles were not assigned dense indices");
|
|
Check(fixture.program.values->memory_info[load_flags.index].resource == 0 &&
|
|
fixture.program.values->memory_info[store_flags.index].resource ==
|
|
0 &&
|
|
fixture.program.values->memory_info[other_flags.index].resource ==
|
|
1,
|
|
"typed memory metadata was not patched to dense indices");
|
|
|
|
std::string error;
|
|
Check(!TrackResources(fixture.program, &error) &&
|
|
error.find("already tracked") != std::string::npos,
|
|
"resource tracking allowed a second mutation pass");
|
|
}
|
|
|
|
void TestScalarAndVectorBufferAlias() {
|
|
Fixture fixture;
|
|
const auto d0 = fixture.UserData(0);
|
|
const auto d1 = fixture.UserData(1);
|
|
const auto d2 = fixture.UserData(2);
|
|
const auto d3 = fixture.UserData(3);
|
|
const auto descriptor = fixture.Buffer({d0, d1, d2, d3}, 4);
|
|
|
|
MemoryInfo scalar;
|
|
scalar.kind = ResourceKind::ScalarBuffer;
|
|
const auto scalar_flags = fixture.AddMemory(scalar, 4);
|
|
fixture.Emit(ValueOpcode::ReadConstBuffer, {descriptor, fixture.UserData(4)},
|
|
scalar_flags);
|
|
MemoryInfo vector;
|
|
vector.kind = ResourceKind::Buffer;
|
|
const auto vector_flags = fixture.AddMemory(vector, 8);
|
|
fixture.Emit(ValueOpcode::LoadBufferU32,
|
|
{descriptor, Value(0u), Value(0u), Value(0u), Value(true)},
|
|
vector_flags);
|
|
fixture.PlanAndTrack();
|
|
|
|
Check(fixture.program.info.buffers.size() == 1 &&
|
|
fixture.program.info.buffers[0].scalar,
|
|
"typed scalar and vector uses of one descriptor were split");
|
|
Check(fixture.program.values->memory_info[scalar_flags.index].resource == 0 &&
|
|
fixture.program.values->memory_info[vector_flags.index].resource ==
|
|
0,
|
|
"scalar/vector alias did not share a dense index");
|
|
}
|
|
|
|
void TestRuntimeUnsignedMinDescriptor() {
|
|
Fixture fixture;
|
|
const auto word3 = fixture.Emit(
|
|
ValueOpcode::UMin32, {fixture.UserData(0), Value(0x100u)});
|
|
const auto descriptor =
|
|
fixture.Buffer({Value(0u), Value(0u), Value(64u), word3}, 0x330);
|
|
MemoryInfo memory;
|
|
memory.kind = ResourceKind::Buffer;
|
|
fixture.Emit(ValueOpcode::LoadBufferU32,
|
|
{descriptor, Value(0u), Value(0u), Value(0u), Value(true)},
|
|
fixture.AddMemory(memory, 0x330));
|
|
fixture.PlanAndTrack();
|
|
|
|
std::array<uint32_t, 1> user_data{0xffffffffu};
|
|
SrtRuntime runtime{.user_data = user_data};
|
|
DescriptorValue value;
|
|
std::string error;
|
|
const auto source = fixture.program.info.buffers[0].source;
|
|
Check(EvaluateDescriptorSource(fixture.program, source, 0x330, runtime, value,
|
|
&error) &&
|
|
value.dwords[3] == 0x100u,
|
|
"runtime descriptor unsigned minimum did not clamp its first operand");
|
|
user_data[0] = 0x80u;
|
|
Check(EvaluateDescriptorSource(fixture.program, source, 0x330, runtime, value,
|
|
&error) &&
|
|
value.dwords[3] == 0x80u,
|
|
"runtime descriptor unsigned minimum did not preserve its first operand");
|
|
}
|
|
|
|
void TestImagesSamplersAndAliases() {
|
|
Fixture fixture;
|
|
std::array<Value, 8> image_words;
|
|
for (uint32_t index = 0; index < image_words.size(); index++) {
|
|
image_words[index] = fixture.UserData(index);
|
|
}
|
|
const auto image_address = fixture.ImageAddress();
|
|
const std::array<Value, 4> sampler0{Value(0u), Value(1u), Value(2u),
|
|
Value(0x1111u)};
|
|
const std::array<Value, 4> sampler1{Value(0u), Value(1u), Value(2u),
|
|
Value(0x2222u)};
|
|
|
|
auto AddSample = [&](uint32_t pc, uint32_t sample_flags,
|
|
const auto &sampler_words) {
|
|
const auto image = fixture.Image(image_words, pc);
|
|
const auto sampler = fixture.Sampler(sampler_words, pc);
|
|
MemoryInfo memory;
|
|
memory.kind = ResourceKind::Image;
|
|
memory.image_dimension = Decoder::ImageDimension::Dim2D;
|
|
memory.image_sample_flags = sample_flags;
|
|
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, image_address},
|
|
fixture.AddMemory(memory, pc));
|
|
return std::pair{image, sampler};
|
|
};
|
|
const auto normal = AddSample(4, 0, sampler0);
|
|
const auto repeated = AddSample(8, 0, sampler1);
|
|
const auto compare = AddSample(12, Decoder::ImageSampleFlagCompare, sampler0);
|
|
|
|
const auto storage = fixture.Image(image_words, 16);
|
|
MemoryInfo storage_memory;
|
|
storage_memory.kind = ResourceKind::StorageImage;
|
|
storage_memory.image_dimension = Decoder::ImageDimension::Dim2D;
|
|
fixture.Emit(ValueOpcode::ImageAtomicIAdd32,
|
|
{storage, image_address, Value(1u), Value(true)},
|
|
fixture.AddMemory(storage_memory, 16));
|
|
|
|
const auto buffer = fixture.Buffer(
|
|
{image_words[0], image_words[1], image_words[2], image_words[3]}, 20);
|
|
MemoryInfo buffer_memory;
|
|
buffer_memory.kind = ResourceKind::Buffer;
|
|
fixture.Emit(ValueOpcode::LoadBufferU32,
|
|
{buffer, Value(0u), Value(0u), Value(0u), Value(true)},
|
|
fixture.AddMemory(buffer_memory, 20));
|
|
fixture.PlanAndTrack();
|
|
|
|
Check(fixture.program.info.images.size() == 3 &&
|
|
fixture.program.info.samplers.size() == 1 &&
|
|
fixture.program.info.sampled_pairs.size() == 2,
|
|
"typed image view classes or samplers were deduplicated incorrectly");
|
|
Check(normal.first.Instruction()->Flags<uint32_t>() ==
|
|
repeated.first.Instruction()->Flags<uint32_t>() &&
|
|
compare.first.Instruction()->Flags<uint32_t>() !=
|
|
normal.first.Instruction()->Flags<uint32_t>(),
|
|
"image handles did not receive view-class indices");
|
|
Check(normal.second.Instruction()->Flags<uint32_t>() == 0 &&
|
|
repeated.second.Instruction()->Flags<uint32_t>() == 0,
|
|
"unused sampler border colors prevented source interning");
|
|
const auto sampler_source = fixture.program.info.samplers[0].source;
|
|
Check(fixture.program.values->descriptor_sources[sampler_source]
|
|
.dwords[3]
|
|
.U32() == 0,
|
|
"unused sampler border color was not canonicalized");
|
|
Check(fixture.program.info.buffers[0].image_alias == 0,
|
|
"buffer/image descriptor alias was not linked");
|
|
}
|
|
|
|
void TestSrtFlatteningAndRuntimeMemoization() {
|
|
Fixture fixture;
|
|
const auto base =
|
|
fixture.Address(fixture.UserData(0), fixture.UserData(1), 4);
|
|
MemoryInfo scalar;
|
|
scalar.kind = ResourceKind::ScalarBuffer;
|
|
scalar.offset = 4;
|
|
const auto read0 = fixture.Emit(ValueOpcode::LoadAddressU32,
|
|
{base, Value(0u), Value(0u), Value(true)},
|
|
fixture.AddMemory(scalar, 4));
|
|
const auto descriptor0 =
|
|
fixture.Buffer({read0, Value(0u), Value(64u), Value(0u)}, 12);
|
|
const auto descriptor1 =
|
|
fixture.Buffer({read0, Value(0u), Value(64u), Value(0u)}, 16);
|
|
MemoryInfo buffer;
|
|
buffer.kind = ResourceKind::Buffer;
|
|
fixture.Emit(ValueOpcode::LoadBufferU32,
|
|
{descriptor0, Value(0u), Value(0u), Value(0u), Value(true)},
|
|
fixture.AddMemory(buffer, 12));
|
|
fixture.Emit(ValueOpcode::LoadBufferU32,
|
|
{descriptor1, Value(0u), Value(0u), Value(0u), Value(true)},
|
|
fixture.AddMemory(buffer, 16));
|
|
fixture.PlanAndTrack();
|
|
|
|
Check(fixture.program.values->srt_reads.size() == 1,
|
|
"shared typed scalar read did not receive one flat SRT slot");
|
|
Check(fixture.program.info.buffers.size() == 1 &&
|
|
fixture.program.info.addresses.empty(),
|
|
"planning-only scalar reads leaked into resource topology");
|
|
Check(fixture.program.values->memory_info[0].planning_only,
|
|
"canonical runtime scalar read was not marked planning-only");
|
|
|
|
std::array<uint32_t, 2> user_data{0x1000u, 0u};
|
|
TestMemory memory;
|
|
memory.words[1] = 0xdeadbeefu;
|
|
SrtRuntime runtime{.user_data = user_data,
|
|
.read_memory = ReadTestMemory,
|
|
.userdata = &memory};
|
|
std::vector<DescriptorValue> descriptors;
|
|
std::vector<uint32_t> flat;
|
|
const DescriptorSourceRequest request{fixture.program.info.buffers[0].source,
|
|
12};
|
|
std::string error;
|
|
Check(EvaluateRuntimeSources(fixture.program, std::span{&request, 1}, runtime,
|
|
descriptors, flat, &error),
|
|
"typed runtime source evaluation failed");
|
|
Check(descriptors.size() == 1 && descriptors[0].dwords[0] == 0xdeadbeefu &&
|
|
flat == std::vector<uint32_t>{0xdeadbeefu} && memory.reads == 1,
|
|
"descriptor and flat SRT evaluation did not share one memoized read");
|
|
|
|
memory.reads = 0;
|
|
memory.fail_after = 0;
|
|
descriptors = {{{1u}, 1u}};
|
|
flat = {2u};
|
|
Check(!EvaluateRuntimeSources(fixture.program, std::span{&request, 1},
|
|
runtime, descriptors, flat, &error) &&
|
|
descriptors == std::vector<DescriptorValue>{{{1u}, 1u}} &&
|
|
flat == std::vector<uint32_t>{2u},
|
|
"runtime evaluation failure was not transactional");
|
|
|
|
ShaderComputeInputInfo compute{};
|
|
Check(CollectShaderInfo(fixture.program, {.compute = &compute}, &error) &&
|
|
AllocateBindings(fixture.program, {}, &error) &&
|
|
FindBinding(fixture.program.bindings,
|
|
DescriptorBindingKind::FlattenedSrt) != nullptr,
|
|
"flattened typed SRT reads did not receive a binding");
|
|
}
|
|
|
|
void TestDynamicSrtReadRemainsExplicit() {
|
|
Fixture fixture;
|
|
const auto base =
|
|
fixture.Address(fixture.UserData(0), fixture.UserData(1), 4);
|
|
MemoryInfo scalar;
|
|
scalar.kind = ResourceKind::ScalarBuffer;
|
|
const auto read =
|
|
fixture.Emit(ValueOpcode::LoadAddressU32,
|
|
{base, fixture.UserData(2), Value(0u), Value(true)},
|
|
fixture.AddMemory(scalar, 4));
|
|
const auto descriptor =
|
|
fixture.Buffer({read, Value(0u), Value(64u), Value(0u)}, 8);
|
|
MemoryInfo buffer;
|
|
buffer.kind = ResourceKind::Buffer;
|
|
fixture.Emit(ValueOpcode::LoadBufferU32,
|
|
{descriptor, Value(0u), Value(0u), Value(0u), Value(true)},
|
|
fixture.AddMemory(buffer, 8));
|
|
fixture.PlanAndTrack();
|
|
|
|
Check(fixture.program.values->srt_reads.empty() &&
|
|
fixture.program.values->dynamic_reads.size() == 1 &&
|
|
fixture.program.info.addresses.size() == 1,
|
|
"dynamic scalar read was incorrectly flattened or lost");
|
|
std::array<uint32_t, 3> user_data{0x1000u, 0u, 4u};
|
|
TestMemory memory;
|
|
memory.words[1] = 0xabcdef01u;
|
|
SrtRuntime runtime{.user_data = user_data,
|
|
.read_memory = ReadTestMemory,
|
|
.userdata = &memory};
|
|
DescriptorValue value;
|
|
std::string error;
|
|
Check(EvaluateDescriptorSource(fixture.program,
|
|
fixture.program.info.buffers[0].source, 8,
|
|
runtime, value, &error) &&
|
|
value.dwords[0] == 0xabcdef01u && memory.reads == 1,
|
|
"dynamic typed scalar descriptor source was not evaluated");
|
|
|
|
ShaderComputeInputInfo compute{};
|
|
Check(CollectShaderInfo(fixture.program, {.compute = &compute}, &error) &&
|
|
AllocateBindings(fixture.program, {}, &error) &&
|
|
FindBinding(fixture.program.bindings,
|
|
DescriptorBindingKind::FlattenedSrt) == nullptr &&
|
|
FindBinding(fixture.program.bindings,
|
|
DescriptorBindingKind::AddressMemory) != nullptr,
|
|
"dynamic scalar read received the wrong resource bindings");
|
|
}
|
|
|
|
void TestPhiValidation() {
|
|
Fixture fixture;
|
|
auto *left = fixture.block;
|
|
auto *right = fixture.AddBlock();
|
|
auto *merge = fixture.AddBlock();
|
|
left->AddBranch(merge);
|
|
right->AddBranch(merge);
|
|
auto &phi = merge->AppendNewInst(ValueOpcode::Phi, {},
|
|
static_cast<uint64_t>(Type::U32));
|
|
phi.AddPhiOperand(left, Value(1u));
|
|
phi.AddPhiOperand(right, Value(2u));
|
|
const auto word3 = fixture.Emit(
|
|
ValueOpcode::UMin32, {Value(&phi), Value(0x100u)}, 0, merge);
|
|
const auto handle =
|
|
fixture.Emit(ValueOpcode::GetBufferResource,
|
|
{Value(0u), Value(0u), Value(0u), word3},
|
|
MemoryFlags{0, 20}, merge);
|
|
MemoryInfo memory;
|
|
memory.kind = ResourceKind::Buffer;
|
|
fixture.Emit(ValueOpcode::LoadBufferU32,
|
|
{handle, Value(0u), Value(0u), Value(0u), Value(true)},
|
|
fixture.AddMemory(memory, 20), merge);
|
|
|
|
std::string error;
|
|
Check(BuildSrtPlan(fixture.program, &error),
|
|
"SRT planning rejected a well-formed typed phi");
|
|
Check(!TrackResources(fixture.program, &error) &&
|
|
error.find("control-dependent phi") != std::string::npos &&
|
|
!fixture.program.resource_tracking_complete &&
|
|
fixture.program.info.buffers.empty() &&
|
|
fixture.program.values->descriptor_sources.empty(),
|
|
"control-dependent descriptor phi was not rejected transactionally");
|
|
}
|
|
|
|
void TestLoopCycleEnteredThroughRuntimeValue() {
|
|
Fixture fixture;
|
|
auto *entry = fixture.block;
|
|
auto *loop = fixture.AddBlock();
|
|
const auto initial = fixture.UserData(0);
|
|
entry->AddBranch(loop);
|
|
loop->AddBranch(loop);
|
|
auto &phi = loop->AppendNewInst(ValueOpcode::Phi, {},
|
|
static_cast<uint64_t>(Type::U32));
|
|
const auto carried = fixture.Emit(
|
|
ValueOpcode::BitwiseAnd32, {Value(&phi), Value(0xffffffffu)}, 0, loop);
|
|
phi.AddPhiOperand(entry, initial);
|
|
phi.AddPhiOperand(loop, carried);
|
|
fixture.Emit(ValueOpcode::GetBufferResource,
|
|
{carried, Value(0u), Value(0u), Value(0u)},
|
|
MemoryFlags{0, 12}, loop);
|
|
|
|
std::string error;
|
|
Check(BuildSrtPlan(fixture.program, &error),
|
|
"SRT planning rejected a valid loop entered through a runtime value");
|
|
}
|
|
|
|
void TestInvariantLoopPhi() {
|
|
Fixture fixture;
|
|
auto *entry = fixture.block;
|
|
auto *loop = fixture.AddBlock();
|
|
entry->AddBranch(loop);
|
|
loop->AddBranch(loop);
|
|
const auto invariant = fixture.UserData(0);
|
|
auto &phi = loop->AppendNewInst(ValueOpcode::Phi, {},
|
|
static_cast<uint64_t>(Type::U32));
|
|
phi.AddPhiOperand(entry, invariant);
|
|
phi.AddPhiOperand(loop, Value(&phi));
|
|
const auto handle = fixture.Emit(
|
|
ValueOpcode::GetBufferResource,
|
|
{Value(&phi), Value(0u), Value(0u), Value(0u)}, MemoryFlags{0, 4}, loop);
|
|
MemoryInfo memory;
|
|
memory.kind = ResourceKind::Buffer;
|
|
fixture.Emit(ValueOpcode::LoadBufferU32,
|
|
{handle, Value(0u), Value(0u), Value(0u), Value(true)},
|
|
fixture.AddMemory(memory, 4), loop);
|
|
fixture.PlanAndTrack();
|
|
|
|
std::array<uint32_t, 1> user_data{0x12345678u};
|
|
SrtRuntime runtime{.user_data = user_data};
|
|
DescriptorValue descriptor;
|
|
std::string error;
|
|
Check(EvaluateDescriptorSource(fixture.program,
|
|
fixture.program.info.buffers[0].source, 4,
|
|
runtime, descriptor, &error) &&
|
|
descriptor.dwords[0] == user_data[0],
|
|
"loop-invariant descriptor phi was not evaluated through typed SSA");
|
|
}
|
|
|
|
void TestAddressMaterializationAndSpecialization() {
|
|
Fixture fixture;
|
|
const auto based =
|
|
fixture.Address(fixture.UserData(0), fixture.UserData(1), 4);
|
|
MemoryInfo global;
|
|
global.kind = ResourceKind::Global;
|
|
global.offset = static_cast<uint32_t>(-8);
|
|
fixture.Emit(ValueOpcode::LoadAddressU32,
|
|
{based, Value(0u), Value(0u), Value(true)},
|
|
fixture.AddMemory(global, 4));
|
|
|
|
const auto undef = fixture.Emit(ValueOpcode::UndefU32);
|
|
const auto unbased = fixture.Address(undef, undef, 8);
|
|
MemoryInfo flat;
|
|
flat.kind = ResourceKind::Flat;
|
|
flat.address_is_full = true;
|
|
fixture.Emit(ValueOpcode::StoreAddressU32,
|
|
{unbased, Value(0u), Value(0u), Value(9u), Value(true)},
|
|
fixture.AddMemory(flat, 8));
|
|
fixture.PlanAndTrack();
|
|
|
|
Check(fixture.program.info.addresses.size() == 2 &&
|
|
!fixture.program.info.addresses[0].unbased &&
|
|
fixture.program.info.addresses[0].min_offset == -8 &&
|
|
fixture.program.info.addresses[1].unbased,
|
|
"typed based and unbased addresses were classified incorrectly");
|
|
std::array<uint32_t, 2> user_data{0x2008u, 0u};
|
|
SrtRuntime runtime{.user_data = user_data, .flat_memory_base = 0x9000u};
|
|
ResourceSnapshot snapshot;
|
|
std::string error;
|
|
Check(MaterializeResources(fixture.program, runtime, snapshot, &error),
|
|
"address resources did not materialize");
|
|
Check(snapshot.addresses.size() == 2 &&
|
|
snapshot.addresses[0].guest_base == 0x2008u &&
|
|
snapshot.addresses[0].binding_base == 0x2000u &&
|
|
snapshot.addresses[1].binding_base == 0x9000u,
|
|
"materialized address windows are incorrect");
|
|
Check(SpecializeResources(fixture.program, snapshot, &error) &&
|
|
fixture.program.info.addresses[0].specialized_base == 8u &&
|
|
fixture.program.info.addresses[1].specialized_base == 0x9000u,
|
|
"typed address specialization was not applied");
|
|
}
|
|
|
|
void TestShaderInfoAndBindingLayout() {
|
|
Fixture fixture;
|
|
const auto handle = fixture.Buffer(
|
|
{fixture.UserData(3), fixture.UserData(4), Value(64u), Value(0u)}, 4);
|
|
MemoryInfo buffer;
|
|
buffer.kind = ResourceKind::Buffer;
|
|
fixture.Emit(ValueOpcode::LoadBufferU32,
|
|
{handle, Value(0u), Value(0u), Value(0u), Value(true)},
|
|
fixture.AddMemory(buffer, 4));
|
|
fixture.Emit(
|
|
ValueOpcode::GetBuiltin,
|
|
{Value(static_cast<uint32_t>(StageInputKind::GlobalInvocationId)),
|
|
Value(2u)});
|
|
fixture.Emit(ValueOpcode::BitwiseXor32, {Value(1u), Value(2u)});
|
|
const auto gds = fixture.Emit(ValueOpcode::GetGdsResource);
|
|
fixture.Emit(ValueOpcode::WriteGdsU32,
|
|
{gds, Value(0u), Value(1u), Value(true)});
|
|
fixture.PlanAndTrack();
|
|
|
|
ShaderComputeInputInfo compute{};
|
|
compute.dispatch_thread_dimensions = true;
|
|
std::string error;
|
|
Check(CollectShaderInfo(fixture.program, {.compute = &compute}, &error),
|
|
"typed shader info collection failed");
|
|
Check(fixture.program.info.has_bitwise_xor &&
|
|
!fixture.program.info.inputs.empty() &&
|
|
fixture.program.info.inputs[0].kind ==
|
|
StageInputKind::GlobalInvocationId,
|
|
"typed shader values were not reflected in shader info");
|
|
|
|
BindingLayoutOptions options;
|
|
options.descriptor_set = 2;
|
|
options.max_push_dwords = 1;
|
|
Check(AllocateBindings(fixture.program, options, &error),
|
|
"typed binding allocation failed");
|
|
Check(fixture.program.bindings.descriptor_set == 2 &&
|
|
FindBinding(fixture.program.bindings,
|
|
DescriptorBindingKind::Buffers) != nullptr &&
|
|
FindBinding(fixture.program.bindings, DescriptorBindingKind::Gds) !=
|
|
nullptr &&
|
|
FindBinding(fixture.program.bindings,
|
|
DescriptorBindingKind::UserData) != nullptr,
|
|
"typed resources were not assigned native bindings");
|
|
Check(fixture.program.bindings.user_data_registers ==
|
|
std::vector<uint32_t>({3u, 4u}),
|
|
"binding layout did not collect live typed user-data values");
|
|
}
|
|
|
|
void TestResourceLimitIsTransactional() {
|
|
Fixture fixture;
|
|
MemoryInfo memory;
|
|
memory.kind = ResourceKind::Buffer;
|
|
for (uint32_t index = 0; index <= ShaderInfo::MaxBuffers; index++) {
|
|
const auto handle = fixture.Buffer(
|
|
{Value(index), Value(index + 1u), Value(index + 2u), Value(index + 3u)},
|
|
index * 4u);
|
|
fixture.Emit(ValueOpcode::LoadBufferU32,
|
|
{handle, Value(0u), Value(0u), Value(0u), Value(true)},
|
|
fixture.AddMemory(memory, index * 4u));
|
|
}
|
|
std::string error;
|
|
Check(BuildSrtPlan(fixture.program, &error),
|
|
"SRT plan failed before resource-limit test");
|
|
Check(!TrackResources(fixture.program, &error) &&
|
|
error.find("buffer resource limit exceeded") != std::string::npos &&
|
|
!fixture.program.resource_tracking_complete &&
|
|
fixture.program.info.buffers.empty() &&
|
|
fixture.program.values->descriptor_sources.empty(),
|
|
"resource-limit failure partially mutated typed resource state");
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main() {
|
|
try {
|
|
const auto Run = [](const char *name, auto test) {
|
|
try {
|
|
test();
|
|
} catch (const std::exception &exception) {
|
|
throw std::runtime_error(std::string(name) + ": " + exception.what());
|
|
}
|
|
};
|
|
Run("dense buffers", TestDenseBufferTracking);
|
|
Run("scalar/vector alias", TestScalarAndVectorBufferAlias);
|
|
Run("runtime unsigned min", TestRuntimeUnsignedMinDescriptor);
|
|
Run("images and samplers", TestImagesSamplersAndAliases);
|
|
Run("SRT runtime", TestSrtFlatteningAndRuntimeMemoization);
|
|
Run("dynamic SRT", TestDynamicSrtReadRemainsExplicit);
|
|
Run("phi validation", TestPhiValidation);
|
|
Run("runtime-rooted loop", TestLoopCycleEnteredThroughRuntimeValue);
|
|
Run("invariant loop phi", TestInvariantLoopPhi);
|
|
Run("address materialization", TestAddressMaterializationAndSpecialization);
|
|
Run("shader info and bindings", TestShaderInfoAndBindingLayout);
|
|
Run("resource limit", TestResourceLimitIsTransactional);
|
|
} catch (const std::exception &exception) {
|
|
std::cerr << "resource tracking test failed: " << exception.what() << '\n';
|
|
return 1;
|
|
}
|
|
std::cout << "resource tracking tests passed\n";
|
|
return 0;
|
|
}
|
|
|
|
// The full emulator supplies these assertion hooks through common. This focused
|
|
// target links only fmt; keep assertion failures observable without widening
|
|
// its legacy build manifest.
|
|
namespace Common {
|
|
int DbgExitIfHandler(const char *expression, const char *file, int line) {
|
|
throw std::runtime_error(std::string("typed IR assertion: ") + expression +
|
|
" at " + file + ':' + std::to_string(line));
|
|
}
|
|
|
|
void DbgExit(int) { throw std::runtime_error("typed IR assertion failed"); }
|
|
} // namespace Common
|
|
|
|
// Keep this focused standalone target self-contained by amalgamating its small
|
|
// typed-IR implementation set.
|
|
#include "graphics/shader/recompiler/ir/Block.cpp"
|
|
#include "graphics/shader/recompiler/ir/Type.cpp"
|
|
#include "graphics/shader/recompiler/ir/Value.cpp"
|
|
#include "graphics/shader/recompiler/ir/opcodes/ValueOpcodes.cpp"
|
|
#include "graphics/shader/recompiler/ir/ValueProgram.cpp"
|