#include "graphics/guest_gpu/gpu_defs.h" #include "graphics/shader/recompiler/ir/ShaderIR.h" #include "graphics/shader/recompiler/ir/passes/BindingLayout.h" #include "graphics/shader/recompiler/ir/passes/DeadCodeElimination.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 #include #include #include #include #include #include #include #include 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); } } template void CheckFatal(F &&function, std::string_view expected, const char *message) { try { function(); } catch (const std::runtime_error &error) { Check(std::string_view(error.what()).find(expected) != std::string_view::npos, message); return; } Check(false, message); } struct Fixture { Program program; Block *block = nullptr; explicit Fixture(ShaderType stage = ShaderType::Compute) { program.stage = stage; program.user_data_count = 64; block = AddBlock(); } Block *AddBlock() { auto storage = std::make_unique(); auto *result = storage.get(); program.block_storage.push_back(std::move(storage)); program.blocks.push_back(result); program.block_info.push_back( {.id = static_cast(program.block_info.size())}); return result; } Value Emit(ValueOpcode opcode, std::initializer_list args = {}, uint64_t flags = 0, Block *destination = nullptr) { if (NumArgsOf(opcode) != std::numeric_limits::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 Value Emit(ValueOpcode opcode, std::initializer_list 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(index))}); } MemoryFlags AddMemory(MemoryInfo memory, uint32_t pc) { const auto index = static_cast(program.memory_info.size()); program.memory_info.push_back(memory); return {index, pc}; } Value Buffer(std::array 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 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 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() { BuildSrtPlan(program); TrackResources(program); } }; struct TestMemory { uint64_t base = 0x1000; std::array words{}; uint32_t reads = 0; uint32_t fail_after = UINT32_MAX; }; bool ReadTestMemory(void *userdata, uint64_t address, std::span values) { auto *memory = static_cast(userdata); if (memory == nullptr || address < memory->base || values.size_bytes() > memory->words.size() * sizeof(uint32_t) || address - memory->base > memory->words.size() * sizeof(uint32_t) - values.size_bytes() || memory->reads >= memory->fail_after) { return false; } std::copy_n(memory->words.begin() + (address - memory->base) / sizeof(uint32_t), values.size(), values.begin()); memory->reads++; return true; } struct LinearTestMemory { uint64_t base = 0x1000; std::vector words = std::vector(0x2200 / 4); uint64_t fail_address = UINT64_MAX; uint64_t watched_address = UINT64_MAX; uint32_t watched_reads = 0; size_t watched_dwords = 0; uint32_t reads = 0; uint32_t descriptor_reads = 0; }; bool ReadLinearTestMemory(void *userdata, uint64_t address, std::span values) { auto *memory = static_cast(userdata); if (memory == nullptr || address < memory->base || values.size_bytes() > memory->words.size() * sizeof(uint32_t) || address - memory->base > memory->words.size() * sizeof(uint32_t) - values.size_bytes() || (address & 3u) != 0u || (memory->fail_address >= address && memory->fail_address - address < values.size_bytes())) { return false; } std::copy_n(memory->words.begin() + (address - memory->base) / sizeof(uint32_t), values.size(), values.begin()); ++memory->reads; if (values.size() == 8u) ++memory->descriptor_reads; if (memory->watched_address >= address && memory->watched_address - address < values.size_bytes()) { ++memory->watched_reads; memory->watched_dwords = values.size(); } return true; } std::unique_ptr MakeIndirectImageFixture(bool malformed, uint32_t material_immediate = 0, bool memory_backed_material = false) { auto fixture = std::make_unique(); std::array material_words; std::array heap_words; for (uint32_t dword = 0; dword < 4; dword++) { material_words[dword] = fixture->UserData(dword); heap_words[dword] = fixture->UserData(dword + 4u); } if (memory_backed_material) { const auto pointer_address = fixture->Address(fixture->UserData(9), fixture->UserData(10), 0x10b0); MemoryInfo pointer_word; pointer_word.kind = ResourceKind::ScalarAddress; const auto pointer = fixture->Emit(ValueOpcode::LoadAddressU32, {pointer_address, Value(0u), Value(0u), Value(true)}, fixture->AddMemory(pointer_word, 0x10b0)); const auto address = fixture->Address(pointer, Value(0u), 0x10c0); MemoryInfo descriptor_word; descriptor_word.kind = ResourceKind::ScalarAddress; material_words[0] = fixture->Emit(ValueOpcode::LoadAddressU32, {address, Value(0u), Value(0u), Value(true)}, fixture->AddMemory(descriptor_word, 0x10c0)); } const auto material = fixture->Buffer(material_words, 0x10d8); const auto heap = fixture->Buffer(heap_words, 0x10d8); if (memory_backed_material) { MemoryInfo shared_buffer; shared_buffer.kind = ResourceKind::Buffer; const auto load = fixture->Emit(ValueOpcode::LoadBufferU32, {material, Value(0u), Value(0u), Value(0u), Value(true)}, fixture->AddMemory(shared_buffer, 0x10d8)); fixture->Emit(ValueOpcode::ReferenceU32, {load}); } const auto invocation = fixture->Emit( ValueOpcode::GetBuiltin, {Value(static_cast(StageInputKind::GlobalInvocationId)), Value(0u)}); const auto selector = fixture->Emit(ValueOpcode::ReadFirstLane, {invocation, Value(true)}); const auto record = fixture->Emit(ValueOpcode::IMul32, {selector, Value(224u)}); const auto member = fixture->Emit(ValueOpcode::IAdd32, {record, Value(4u)}); fixture->Emit(ValueOpcode::ReferenceU32, {record}); fixture->Emit(ValueOpcode::ReferenceU32, {member}); MemoryInfo material_scalar; material_scalar.kind = ResourceKind::ScalarBuffer; material_scalar.offset = material_immediate; const auto key = fixture->Emit(ValueOpcode::ReadConstBuffer, {material, member}, fixture->AddMemory(material_scalar, 0x10d8)); const auto heap_offset = fixture->Emit(ValueOpcode::ShiftLeftLogical32, {key, Value(5u)}); std::array image_words; MemoryInfo heap_scalar; heap_scalar.kind = ResourceKind::ScalarBuffer; for (uint32_t dword = 0; dword < image_words.size(); dword++) { auto component = heap_scalar; component.offset = dword * sizeof(uint32_t); if (malformed && dword == image_words.size() - 1u) { component.offset += sizeof(uint32_t); } image_words[dword] = fixture->Emit(ValueOpcode::ReadConstBuffer, {heap, heap_offset}, fixture->AddMemory(component, 0x10d8)); } const auto image = fixture->Image(image_words, 0x10f0); const auto sampler = fixture->Sampler({Value(0u), Value(0u), Value(0u), Value(0u)}, 0x10f0); MemoryInfo sample; sample.kind = ResourceKind::Image; sample.image_dimension = Decoder::ImageDimension::Dim2D; const auto sampled = fixture->Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture->ImageAddress()}, fixture->AddMemory(sample, 0x10f0)); const auto sampled_x = fixture->Emit(ValueOpcode::CompositeExtractU32x4, {sampled, Value(0u)}); fixture->Emit(ValueOpcode::ReferenceU32, {sampled_x}); return fixture; } void TestInvariantIndirectImageMaterialization() { auto fixture = MakeIndirectImageFixture(false); fixture->PlanAndTrack(); auto resource_plan = ExtractResourcePlan(fixture->program); EliminateDeadCode(fixture->program.blocks); ValidateProgram(fixture->program, true); Check(fixture->program.info.buffers.size() == 1 && fixture->program.info.images.size() == 1 && fixture->program.dynamic_reads.size() == 1, "indirect image key was not retained as a scalar-buffer read"); const auto source = fixture->program.info.images[0].source; Check(source < fixture->program.descriptor_sources.size() && fixture->program.descriptor_sources[source] .indirect_image.has_value(), "indirect image source was not retained for runtime proof"); const auto image_handle = std::ranges::find_if(*fixture->block, [](const Inst &inst) { return inst.GetOpcode() == ValueOpcode::GetImageResource; }); Check(image_handle != fixture->block->end() && image_handle->Arg(0).ResolveInstruction() != nullptr && image_handle->Arg(0).ResolveInstruction()->GetOpcode() == ValueOpcode::ReadConstBuffer, "indirect image handle discarded the live material key"); std::array user_data{0x1000u, 224u << 16u, 2u, 0u, 0x2000u, 16u << 16u, 4u, 0u, 7u}; LinearTestMemory memory; std::array image_descriptor{}; image_descriptor[0] = 0x20u; image_descriptor[1] = static_cast( Libs::Graphics::Prospero::BufferFormat::k32_32_32_32Float) << 20u; image_descriptor[2] = 3u | (3u << 14u); image_descriptor[3] = Libs::Graphics::DstSel(4, 5, 6, 7) | (static_cast(Libs::Graphics::Prospero::ImageType::kColor2D) << 28u); for (uint32_t dword = 0; dword < image_descriptor.size(); dword++) { memory.words[(0x2000u - memory.base) / 4u + dword] = image_descriptor[dword]; memory.words[(0x2020u - memory.base) / 4u + dword] = image_descriptor[dword]; } memory.words[(0x2020u - memory.base) / 4u] ^= 1u; SrtRuntime runtime{.user_data = user_data, .userdata = &memory, .read_specialization_memory = ReadLinearTestMemory}; ResourceSnapshot snapshot; ResourceSpecialization specialization; Check(MaterializeResources(resource_plan, runtime, snapshot, specialization) && snapshot.images.size() == 1 && std::equal(image_descriptor.begin(), image_descriptor.end(), snapshot.images[0].dwords.begin()), "invariant indirect image table did not materialize"); user_data[5] = 0u; user_data[6] = 19u; memory.fail_address = 0x2010u; memory.watched_address = 0x2000u; Check(MaterializeResources(resource_plan, runtime, snapshot, specialization) && memory.watched_dwords == 4u && std::equal(image_descriptor.begin(), image_descriptor.end(), snapshot.images[0].dwords.begin()), "partial scalar-buffer descriptor read crossed bounds instead of zeroing its tail"); memory.fail_address = 0x2008u; Check(!MaterializeResources(resource_plan, runtime, snapshot, specialization), "unreadable memory inside the descriptor prefix was accepted"); user_data[5] = 16u << 16u; user_data[6] = 4u; memory.watched_address = UINT64_MAX; memory.fail_address = 0x1004u; Check(!MaterializeResources(resource_plan, runtime, snapshot, specialization), "unreadable material-table selector was accepted"); memory.fail_address = UINT64_MAX; memory.words[(0x1000u - memory.base + 36u) / 4u] = 1u; for (uint32_t dword = 0; dword < image_descriptor.size(); dword++) { memory.words[(0x2000u - memory.base) / 4u + dword] = 0u; memory.words[(0x2020u - memory.base) / 4u + dword] = 0u; } memory.words[(0x2000u - memory.base) / 4u + 1u] = image_descriptor[1]; memory.words[(0x2000u - memory.base) / 4u + 3u] = image_descriptor[3]; memory.words[(0x2020u - memory.base) / 4u + 1u] = image_descriptor[1]; memory.words[(0x2020u - memory.base) / 4u + 3u] = image_descriptor[3] ^ (1u << 28u); ResourceSnapshot null_snapshot; ResourceSpecialization null_specialization; Check(MaterializeResources(resource_plan, runtime, null_snapshot, null_specialization) && std::ranges::all_of(null_snapshot.images[0].dwords, [](uint32_t dword) { return dword == 0u; }), "stale typed null image descriptors were not canonicalized"); for (uint32_t dword = 0; dword < image_descriptor.size(); dword++) { memory.words[(0x2000u - memory.base) / 4u + dword] = image_descriptor[dword]; memory.words[(0x2020u - memory.base) / 4u + dword] = image_descriptor[dword]; } memory.words[(0x2020u - memory.base) / 4u] ^= 1u; memory.words[(0x1000u - memory.base + 36u) / 4u] = 1u; ResourceSnapshot dynamic_snapshot; ResourceSpecialization dynamic_specialization; Check(MaterializeResources(resource_plan, runtime, dynamic_snapshot, dynamic_specialization) && dynamic_snapshot.images.size() == 2 && dynamic_specialization.images.size() == 2, "dynamic indirect image table did not materialize"); const auto second_image = (0x2020u - memory.base) / 4u; memory.words[second_image + 1u] |= 3u << 30u; memory.words[second_image + 2u] = 3u << 14u; memory.words[second_image + 3u] = Libs::Graphics::DstSel(4, 5, 6, 7) | (static_cast(Libs::Graphics::Prospero::ImageType::kCube) << 28u); memory.words[second_image + 4u] = 11u; ResourceSnapshot mixed_snapshot; ResourceSpecialization mixed_specialization; Check(MaterializeResources(resource_plan, runtime, mixed_snapshot, mixed_specialization) && mixed_snapshot.images.size() == 2 && mixed_specialization.images.size() == 2 && mixed_specialization.images[0].dimension == Decoder::ImageDimension::Dim2D && !mixed_specialization.images[0].cube && mixed_specialization.images[1].dimension == Decoder::ImageDimension::Dim2DArray && mixed_specialization.images[1].cube && std::equal(mixed_snapshot.images[1].dwords.begin(), mixed_snapshot.images[1].dwords.end(), memory.words.begin() + second_image), "mixed 2D and cube candidates were rejected or discarded"); memory.words[second_image + 1u] = static_cast( Libs::Graphics::Prospero::BufferFormat::k32_32_32_32UInt) << 20u | (3u << 30u); Check(!MaterializeResources(resource_plan, runtime, mixed_snapshot, mixed_specialization), "indirect images with different numeric classes were accepted"); for (const auto dword : {1u, 2u, 3u, 4u}) { memory.words[second_image + dword] = image_descriptor[dword]; } ApplyResourceSpecialization(fixture->program, dynamic_specialization); Check(fixture->program.info.images.size() == 2 && fixture->program.info.images[0].indirect_root == 0 && fixture->program.info.images[0].indirect_search_iterations != 0 && fixture->program.info.images[0].indirect_resources.size() == 2 && dynamic_snapshot.images.size() == 2, "dynamic indirect image table was not specialized transactionally"); const auto &mapping = dynamic_specialization.images[0]; const auto key_count = dynamic_snapshot.flattened_srt[mapping.indirect_mapping_offset]; Check(mapping.indirect_search_iterations == std::bit_width(key_count) && mapping.indirect_mapping_offset + 1u + key_count * 2u == dynamic_snapshot.flattened_srt.size(), "indirect image mapping retained worst-case padding"); for (uint32_t dword = 0; dword < image_descriptor.size(); dword++) { memory.words[(0x2000u - memory.base) / 4u + dword] = image_descriptor[dword]; memory.words[(0x2020u - memory.base) / 4u + dword] = image_descriptor[dword]; } memory.words[(0x2000u - memory.base) / 4u] += 0x100u; memory.words[(0x2020u - memory.base) / 4u] += 0x101u; ResourceSnapshot rebound_snapshot; ResourceSpecialization rebound_specialization; Check(MaterializeResources(resource_plan, runtime, rebound_snapshot, rebound_specialization) && rebound_specialization == dynamic_specialization, "stable indirect key mapping did not accept changed image addresses"); memory.words[(0x2020u - memory.base) / 4u] = memory.words[(0x2000u - memory.base) / 4u]; Check(MaterializeResources(resource_plan, runtime, rebound_snapshot, rebound_specialization) && rebound_specialization != dynamic_specialization, "collapsed indirect candidates did not select a new specialization"); const auto collapsed_specialization = rebound_specialization; ResourceSnapshot capacity_snapshot; ResourceSpecialization capacity_specialization; for (const uint32_t records : {1u, 3u}) { user_data[2] = records; Check(MaterializeResources(resource_plan, runtime, capacity_snapshot, capacity_specialization), "runtime indirect key mapping rejected a valid material-table size"); } user_data[2] = 2u; memory.words[(0x2020u - memory.base) / 4u] = memory.words[(0x2000u - memory.base) / 4u] + 1u; memory.words[(0x2040u - memory.base) / 4u] = memory.words[(0x2000u - memory.base) / 4u] + 2u; for (uint32_t dword = 1; dword < image_descriptor.size(); dword++) { memory.words[(0x2040u - memory.base) / 4u + dword] = image_descriptor[dword]; } memory.words[(0x1000u - memory.base + 68u) / 4u] = 2u; Check(MaterializeResources(resource_plan, runtime, rebound_snapshot, rebound_specialization) && rebound_specialization != collapsed_specialization, "larger indirect candidate topology reused the old specialization"); auto memory_backed = MakeIndirectImageFixture(false, 0u, true); memory_backed->PlanAndTrack(); auto memory_backed_plan = ExtractResourcePlan(memory_backed->program); EliminateDeadCode(memory_backed->program.blocks); std::array memory_backed_user_data{0x1000u, 224u << 16u, 2u, 0u, 0x2000u, 16u << 16u, 4u, 0u, 7u, 0x3100u, 0u}; memory.words[(0x3100u - memory.base) / 4u] = 0x3000u; memory.words[(0x3000u - memory.base) / 4u] = 0x1000u; memory.fail_address = 0x3100u; SrtRuntime memory_backed_runtime{.user_data = memory_backed_user_data, .userdata = &memory, .read_specialization_memory = ReadLinearTestMemory}; Check(!MaterializeResources(memory_backed_plan, memory_backed_runtime, snapshot, specialization), "unreadable indirect table descriptor was accepted"); memory.fail_address = UINT64_MAX; memory.watched_address = 0x3100u; memory.watched_reads = 0; Check(MaterializeResources(memory_backed_plan, memory_backed_runtime, snapshot, specialization) && memory.watched_reads == 1u, "descriptor, flattened SRT and indirect-table roots repeated a clean pointer read"); const auto* buffer_storage = snapshot.buffers.data(); const auto* image_storage = snapshot.images.data(); const auto* flat_storage = snapshot.flattened_srt.data(); const auto* user_data_storage = snapshot.user_data.data(); const auto* buffer_specialization_storage = specialization.buffers.data(); const auto* image_specialization_storage = specialization.images.data(); const auto old_address = snapshot.images[0].dwords[0]; memory.words[(0x2000u - memory.base) / 4u] += 0x100u; memory.watched_reads = 0; Check(MaterializeResources(memory_backed_plan, memory_backed_runtime, snapshot, specialization) && memory.watched_reads == 1u && snapshot.images[0].dwords[0] == old_address + 0x100u, "cache refresh reused stale table contents or repeated its clean pointer read"); Check(snapshot.buffers.data() == buffer_storage && snapshot.images.data() == image_storage && snapshot.flattened_srt.data() == flat_storage && snapshot.user_data.data() == user_data_storage && specialization.buffers.data() == buffer_specialization_storage && specialization.images.data() == image_specialization_storage, "a same-shape refresh discarded the runtime output storage"); auto malformed = MakeIndirectImageFixture(true); BuildSrtPlan(malformed->program); CheckFatal([&] { TrackResources(malformed->program); }, "not a valid runtime value", "malformed indirect image pattern was accepted"); Check(!malformed->program.resource_tracking_complete && malformed->program.info.images.empty() && malformed->program.descriptor_sources.empty(), "malformed indirect image pattern was partially accepted"); auto wrapped_immediate = MakeIndirectImageFixture(false, 4u); BuildSrtPlan(wrapped_immediate->program); CheckFatal([&] { TrackResources(wrapped_immediate->program); }, "not a valid runtime value", "wrapped scalar immediate entered the invariant image proof"); Check(!wrapped_immediate->program.resource_tracking_complete, "wrapped scalar immediate entered the invariant image proof"); } void TestGuardedDirectImageTable() { namespace CFG = Libs::Graphics::ShaderRecompiler::CFG; enum class Guard { Nonzero, Zero, Unrelated, Bypass }; const auto make_plan = [](Guard guard) { Fixture fixture(ShaderType::Pixel); auto *entry = fixture.block; auto *middle = fixture.AddBlock(); auto *before_sample = fixture.AddBlock(); auto *sample = fixture.AddBlock(); auto *exit = fixture.AddBlock(); entry->AddBranch(middle); entry->AddBranch(exit); middle->AddBranch(before_sample); before_sample->AddBranch(sample); sample->AddBranch(exit); if (guard == Guard::Bypass) exit->AddBranch(sample); const auto mask = fixture.Emit(ValueOpcode::ReadFirstLane, {fixture.Emit(ValueOpcode::GetAttribute, {Value(0u), Value(0u)}), Value(true)}); const auto nonzero = fixture.Emit( ValueOpcode::INotEqual32, {Value(0u), guard == Guard::Unrelated ? fixture.UserData(2) : mask}); fixture.program.block_info[0].condition = fixture.Emit(ValueOpcode::LogicalNot, {nonzero}); fixture.program.block_info[0].terminator = { .kind = CFG::TerminatorKind::ConditionalBranch, .true_block = guard == Guard::Zero ? 1u : 4u, .false_block = guard == Guard::Zero ? 4u : 1u}; for (uint32_t block = 1; block < 4; ++block) { fixture.program.block_info[block].terminator = { .kind = CFG::TerminatorKind::Branch, .true_block = block + 1u}; } fixture.program.block_info[4].terminator = { .kind = guard == Guard::Bypass ? CFG::TerminatorKind::Branch : CFG::TerminatorKind::Return, .true_block = 3u}; const auto srt = fixture.Address(fixture.UserData(0), fixture.UserData(1)); std::array pointer; for (uint32_t word = 0; word < pointer.size(); ++word) { MemoryInfo memory; memory.kind = ResourceKind::ScalarAddress; memory.offset = word * 4u; pointer[word] = fixture.Emit( ValueOpcode::LoadAddressU32, {srt, Value(0u), Value(0u), Value(true)}, fixture.AddMemory(memory, 0x20)); } fixture.block = sample; const auto key = fixture.Emit(ValueOpcode::FindILsb32, {mask}); const auto offset = fixture.Emit(ValueOpcode::IAdd32, {fixture.Emit(ValueOpcode::ShiftLeftLogical32, {key, Value(5u)}), Value(344u)}); std::array words; for (uint32_t group = 0; group < 2u; ++group) { // Separate equivalent pointer handles mirror the two scalar x4 loads. const auto table = fixture.Address(pointer[0], pointer[1]); const auto group_offset = group == 0u ? offset : fixture.Emit( ValueOpcode::IAdd32, {offset, Value(16u)}); for (uint32_t word = 0; word < 4u; ++word) { MemoryInfo memory; memory.kind = ResourceKind::ScalarAddress; memory.offset = word * 4u; words[group * 4u + word] = fixture.Emit( ValueOpcode::LoadAddressU32, {table, group_offset, Value(0u), Value(true)}, fixture.AddMemory(memory, 0x100 + group * 8u)); } } const auto image = fixture.Image(words, 0x128); const auto sampler = fixture.Sampler({Value(0u), Value(0u), Value(0u), Value(0u)}); MemoryInfo memory; memory.kind = ResourceKind::Image; memory.image_dimension = Decoder::ImageDimension::Dim2D; fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress()}, fixture.AddMemory(memory, 0x128)); fixture.PlanAndTrack(); const auto source = fixture.program.info.images[0].source; const auto &indirect = fixture.program.descriptor_sources[source].indirect_image; Check(indirect && indirect->material_source == UINT32_MAX && indirect->selector_stride == 0u && indirect->table_offset == 344u && indirect->key_count == 32u && fixture.program.descriptor_sources[indirect->table_source].dword_count == 2u, "guarded direct image table lost its pointer or proven selector range"); return ExtractResourcePlan(fixture.program); }; auto plan = make_plan(Guard::Nonzero); for (const auto guard : {Guard::Zero, Guard::Unrelated, Guard::Bypass}) { CheckFatal([&] { make_plan(guard); }, "not a valid runtime value", "direct table accepted a selector without a dominating nonzero guard"); } LinearTestMemory memory; constexpr uint64_t table = 0x1800u + 344u; memory.words[0] = 0x1800u; const auto fill_table = [](LinearTestMemory &memory, uint64_t base) { for (uint32_t key = 0; key < 32u; ++key) { const auto word = (base - memory.base) / 4u + key * 8u; memory.words[word] = 0x100u + key; memory.words[word + 1u] = static_cast( Libs::Graphics::Prospero::BufferFormat::k32_32_32_32Float) << 20u; memory.words[word + 3u] = Libs::Graphics::DstSel(4, 5, 6, 7) | (static_cast(Libs::Graphics::Prospero::ImageType::kColor2D) << 28u); } }; fill_table(memory, table); std::array user_data{0x1000u, 0u}; SrtRuntime runtime{.user_data = user_data, .read_memory = ReadLinearTestMemory, .userdata = &memory, .read_specialization_memory = ReadLinearTestMemory}; ResourceSnapshot snapshot; ResourceSpecialization specialization; Check(MaterializeResources(plan, runtime, snapshot, specialization) && snapshot.images.size() == 32u && specialization.images.size() == 32u && snapshot.flattened_srt[specialization.images[0].indirect_mapping_offset] == 32u && memory.reads == 34u && memory.descriptor_reads == 32u, "direct table did not retain all 32 reachable descriptors"); const auto captured_word = (table - memory.base) / 4u + 16u * 8u; const auto original_descriptor = snapshot.images[16].dwords; const std::array captured_invalid{ 0x101f0000u, 0xcb500000u, 0x001fc01fu, 0xd0970facu, 0x86000000u, 0x00500003u, 0x00000400u, 0x00005204u}; std::copy(captured_invalid.begin(), captured_invalid.end(), memory.words.begin() + captured_word); Check(MaterializeResources(plan, runtime, snapshot, specialization) && snapshot.images.size() == 32u && snapshot.flattened_srt[specialization.images[0].indirect_mapping_offset] == 32u && std::ranges::all_of(snapshot.images[16].dwords, [](uint32_t word) { return word == 0u; }), "captured non-descriptor record became a host image or lost its key mapping"); std::copy(original_descriptor.begin(), original_descriptor.end(), memory.words.begin() + captured_word); memory.words[(table - memory.base) / 4u + 31u * 8u] = 0x987u; Check(MaterializeResources(plan, runtime, snapshot, specialization) && snapshot.images[31].dwords[0] == 0x987u, "direct table refresh reused stale descriptor contents"); memory.fail_address = table + 31u * 32u + 28u; Check(!MaterializeResources(plan, runtime, snapshot, specialization), "direct table accepted an unreadable final descriptor word"); LinearTestMemory wrapping; wrapping.base = 0u; wrapping.words[0x1000u / 4u] = 0xffffff00u; wrapping.words[0x1000u / 4u + 1u] = 0xffffu; wrapping.watched_address = 88u; fill_table(wrapping, 88u); runtime.userdata = &wrapping; Check(!MaterializeResources(plan, runtime, snapshot, specialization) && wrapping.watched_reads == 0u, "direct table wrapped its descriptor address at the 48-bit boundary"); LinearTestMemory endpoint; endpoint.base = (uint64_t{1} << 48u) - 0x1000u; const auto crossing = (uint64_t{1} << 48u) - 16u; endpoint.words[0] = static_cast(crossing - 344u); endpoint.words[1] = static_cast((crossing - 344u) >> 32u); fill_table(endpoint, crossing); endpoint.watched_address = crossing; user_data = {static_cast(endpoint.base), static_cast(endpoint.base >> 32u)}; runtime.userdata = &endpoint; Check(!MaterializeResources(plan, runtime, snapshot, specialization) && endpoint.watched_reads == 0u, "batched descriptor read crossed the 48-bit endpoint"); } void TestImageDescriptorFields() { constexpr std::array, 5> reserved{ {{1u, 0x20000000u}, {2u, 0x70003000u}, {4u, 0xe000e000u}, {5u, 0xf9000000u}, {6u, 0x00007b00u}}}; for (const bool r128 : {false, true}) { Fixture fixture; std::array words; for (uint32_t word = 0; word < words.size(); ++word) { words[word] = fixture.UserData(word); } const auto image = fixture.Image(words); const auto sampler = fixture.Sampler({Value(0u), Value(0u), Value(0u), Value(0u)}); MemoryInfo memory; memory.kind = ResourceKind::Image; memory.image_dimension = Decoder::ImageDimension::Dim2D; memory.image_r128 = r128; fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress()}, fixture.AddMemory(memory, 0x80)); fixture.PlanAndTrack(); auto plan = ExtractResourcePlan(fixture.program); std::array user_data{}; user_data[0] = 0x100u; user_data[1] = static_cast( Libs::Graphics::Prospero::BufferFormat::k32_32_32_32Float) << 20u; user_data[3] = Libs::Graphics::DstSel(4, 5, 6, 7) | (static_cast(Libs::Graphics::Prospero::ImageType::kColor2D) << 28u); const SrtRuntime runtime{.user_data = user_data}; ResourceSnapshot snapshot; ResourceSpecialization specialization; const auto is_null = [&] { return std::ranges::all_of(snapshot.images[0].dwords, [](uint32_t word) { return word == 0u; }); }; Check(MaterializeResources(plan, runtime, snapshot, specialization) && snapshot.images[0].dwords == user_data, "valid texture descriptor was rejected"); const auto valid_descriptor = user_data; // Keep RESOURCE_LEVEL set in this valid texture descriptor. user_data = {0x0208a200u, 0xca900000u, 0x800fc00fu, 0x90960facu, 0u, 0x60u, 0u, 0u}; Check(MaterializeResources(plan, runtime, snapshot, specialization) && snapshot.images[0].dwords == user_data, "instruction-ready texture descriptor lost RESOURCE_LEVEL or became null"); user_data = valid_descriptor; for (const auto [word, mask] : reserved) { for (uint32_t bits = mask; bits != 0u; bits &= bits - 1u) { const auto bit = bits & (0u - bits); user_data[word] |= bit; Check(MaterializeResources(plan, runtime, snapshot, specialization) && (r128 && word >= 4u ? snapshot.images[0].dwords == user_data : is_null()), "reserved descriptor bits or ignored R128 upper words were misclassified"); user_data[word] &= ~bit; } } user_data[5] = 0x06800000u; user_data[6] = 0x010880ffu; user_data[7] = 0x1234u; Check(MaterializeResources(plan, runtime, snapshot, specialization) && snapshot.images[0].dwords == user_data, "defined mip-statistics, PRT, or metadata fields were treated as reserved"); user_data[3] |= 3u << 16u; Check(MaterializeResources(plan, runtime, snapshot, specialization) && snapshot.images[0].dwords == user_data, "view LAST_LEVEL above physical MAX_MIP was rejected"); if (!r128) { user_data[3] = (user_data[3] & 0x0fffffffu) | (static_cast(Libs::Graphics::Prospero::ImageType::kColor2DArray) << 28u); user_data[4] = 3u | (4u << 16u); Check(MaterializeResources(plan, runtime, snapshot, specialization) && is_null(), "array view starting after its last slice was accepted"); for (const auto base : {1u, 3u}) { user_data[4] = 3u | (base << 16u); Check(MaterializeResources(plan, runtime, snapshot, specialization) && snapshot.images[0].dwords == user_data, "valid array view with a nonzero base slice was rejected"); } } } } void TestUniformScalarBufferImage() { Fixture fixture(ShaderType::Pixel); std::array material_words; std::array heap_words; for (uint32_t dword = 0; dword < 4; dword++) { material_words[dword] = fixture.UserData(dword); heap_words[dword] = fixture.UserData(dword + 4u); } const auto material = fixture.Buffer(material_words); const auto heap = fixture.Buffer(heap_words); const auto selector = fixture.Emit(ValueOpcode::ShiftLeftLogical32, {fixture.UserData(8), Value(2u)}); MemoryInfo scalar; scalar.kind = ResourceKind::ScalarBuffer; scalar.offset = 0x40u; const auto key = fixture.Emit(ValueOpcode::ReadConstBuffer, {material, selector}, fixture.AddMemory(scalar, 0x100)); const auto record = fixture.Emit(ValueOpcode::IMul32, {key, Value(48u)}); std::array image_words; for (uint32_t dword = 0; dword < image_words.size(); dword++) { scalar.offset = 0x10u + dword * sizeof(uint32_t); image_words[dword] = fixture.Emit(ValueOpcode::ReadConstBuffer, {heap, record}, fixture.AddMemory(scalar, 0x200)); } const auto image = fixture.Image(image_words); const auto sampler = fixture.Sampler({Value(0u), Value(0u), Value(0u), Value(0u)}); MemoryInfo sample; sample.kind = ResourceKind::Image; sample.image_dimension = Decoder::ImageDimension::Dim2D; fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress()}, fixture.AddMemory(sample, 0x228)); fixture.PlanAndTrack(); const auto plan = ExtractResourcePlan(fixture.program); std::array user_data{0x1000u, 0u, 0x100u, 0u, 0x2000u, 0u, 0x100u, 0u, 0u}; LinearTestMemory memory; memory.words[0x44u / 4u] = 1u; std::array descriptor{}; descriptor[0] = 0x20u; descriptor[1] = static_cast( Libs::Graphics::Prospero::BufferFormat::k32_32_32_32Float) << 20u; descriptor[2] = 3u | (3u << 14u); descriptor[3] = Libs::Graphics::DstSel(4, 5, 6, 7) | (static_cast(Libs::Graphics::Prospero::ImageType::kColor2D) << 28u); for (uint32_t index = 0; index < 2; index++) { std::copy(descriptor.begin(), descriptor.end(), memory.words.begin() + (0x1010u + index * 48u) / 4u); memory.words[(0x1010u + index * 48u) / 4u] += index; } SrtRuntime runtime{.user_data = user_data, .read_memory = ReadLinearTestMemory, .userdata = &memory, .read_specialization_memory = ReadLinearTestMemory}; ResourceSnapshot snapshot; ResourceSpecialization specialization; const auto materializes = [&](uint32_t address) { return MaterializeResources(plan, runtime, snapshot, specialization) && snapshot.images.size() == 1 && snapshot.images[0].dwords[0] == address; }; Check(materializes(0x20u), "nested draw-uniform image descriptor did not materialize"); user_data[8] = 1u; Check(materializes(0x21u), "changed draw selector reused the previous image descriptor"); memory.words[0x44u / 4u] = 0u; memory.words[0x1010u / 4u] = 0x30u; Check(materializes(0x30u), "changed scalar table memory reused the previous image descriptor"); memory.fail_address = 0x1044u; Check(!MaterializeResources(plan, runtime, snapshot, specialization), "unavailable nested image table was accepted"); } void TestComputeBufferFill() { struct Options { bool scalar = false; bool conditional = false; bool shifted = false; bool extra_store = false; bool clean = false; bool branch = false; }; const auto Run = [](Options options) { Fixture fixture; fixture.program.block_info[0].terminator.kind = Libs::Graphics::ShaderRecompiler::CFG::TerminatorKind::Return; if (options.branch) { fixture.program.block_info[0].terminator.kind = Libs::Graphics:: ShaderRecompiler::CFG::TerminatorKind::ConditionalBranch; } const auto buffer = fixture.Buffer({fixture.UserData(0), fixture.UserData(1), fixture.UserData(2), fixture.UserData(3)}); const auto local = fixture.Emit( ValueOpcode::GetBuiltin, {Value(static_cast(StageInputKind::LocalInvocationId)), Value(0u)}); const auto group = fixture.Emit( ValueOpcode::GetBuiltin, {Value(static_cast(StageInputKind::WorkgroupId)), Value(0u)}); auto index = fixture.Emit(ValueOpcode::IAdd32, {local, fixture.Emit(ValueOpcode::ShiftLeftLogical32, {group, Value(6u)})}); if (options.shifted) index = fixture.Emit(ValueOpcode::IAdd32, {index, Value(1u)}); Value value(0u); TestMemory memory; memory.words[0] = 0x40404040u; if (options.scalar) { const auto input = fixture.Buffer({Value(static_cast(memory.base)), Value(4u << 16), Value(1u), Value(0x14204u)}); MemoryInfo load; load.kind = ResourceKind::ScalarBuffer; value = fixture.Emit(ValueOpcode::ReadConstBuffer, {input, Value(0u)}, fixture.AddMemory(load, 8)); } MemoryInfo store; store.kind = ResourceKind::Buffer; store.formatted = true; store.idxen = true; const auto flags = fixture.AddMemory(store, 16); const auto predicate = options.conditional ? fixture.Emit(ValueOpcode::ULessThan32, {local, Value(32u)}) : Value(true); const auto EmitStore = [&] { fixture.Emit(ValueOpcode::StoreBufferU32, {buffer, index, Value(0u), Value(0u), value, predicate}, flags); }; EmitStore(); if (options.extra_store) EmitStore(); fixture.PlanAndTrack(); auto plan = ExtractResourcePlan(fixture.program); std::array userdata{0x200000u, 4u << 16, 0x4000u, 0x14204u}; ResourceSnapshot snapshot; ResourceSpecialization specialization; const auto Read = +[](void *data, uint64_t address, std::span words) { auto &memory = *static_cast(data); if (address != memory.base || words.size() != 1u) return false; ++memory.reads; words[0] = memory.words[0]; return true; }; Check(MaterializeResources( plan, {.user_data = userdata, .read_memory = Read, .userdata = &memory, .read_specialization_memory = options.clean ? Read : nullptr}, snapshot, specialization), "fill fixture did not materialize"); const bool expected = !options.conditional && !options.shifted && !options.extra_store && !options.branch && (!options.scalar || options.clean); Check((snapshot.uniform_fill.words != 0) == expected, "fill proof accepted an unsafe store or missed the real GTA3 clear"); if (expected) { Check(snapshot.uniform_fill.words == 1 && snapshot.uniform_fill.group_stride[0] == 64 && snapshot.uniform_fill.value == (options.scalar ? 0x40404040u : 0u), "fill proof lost address coverage or the actual stored scalar"); if (options.scalar && options.clean) { Check(MaterializeResources(plan, {.user_data = userdata, .read_memory = Read, .userdata = &memory}, snapshot, specialization) && snapshot.uniform_fill.words == 0, "an unavailable clean value retained a previous uniform fill"); } } }; Run({}); Run({.scalar = true, .clean = true}); Run({.scalar = true}); Run({.conditional = true, .clean = true}); Run({.shifted = true, .clean = true}); Run({.extra_store = true, .clean = true}); Run({.clean = true, .branch = true}); } void TestDenseBufferTracking() { Fixture fixture; std::array 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.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() == 0 && second.Instruction()->Flags() == 1, "typed handles were not assigned dense indices"); Check(fixture.program.memory_info[load_flags.index].resource == 0 && fixture.program.memory_info[store_flags.index].resource == 0 && fixture.program.memory_info[other_flags.index].resource == 1, "typed memory metadata was not patched to dense indices"); CheckFatal([&] { TrackResources(fixture.program); }, "already tracked", "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.memory_info[scalar_flags.index].resource == 0 && fixture.program.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 user_data{0xffffffffu}; SrtRuntime runtime{.user_data = user_data}; DescriptorValue value; const auto source = fixture.program.info.buffers[0].source; Check(SrtWalker(fixture.program, runtime).EvaluateDescriptor(source, value) && value.dwords[3] == 0x100u, "runtime descriptor unsigned minimum did not clamp its first operand"); user_data[0] = 0x80u; Check( SrtWalker(fixture.program, runtime).EvaluateDescriptor(source, value) && value.dwords[3] == 0x80u, "runtime descriptor unsigned minimum did not preserve its first operand"); } void TestImagesSamplersAndAliases() { Fixture fixture; std::array 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 sampler0{Value(0u), Value(1u), Value(2u), Value(0x1111u)}; const std::array 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::Image; 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() == repeated.first.Instruction()->Flags() && compare.first.Instruction()->Flags() != normal.first.Instruction()->Flags(), "image handles did not receive view-class indices"); Check(normal.second.Instruction()->Flags() == 0 && repeated.second.Instruction()->Flags() == 0, "unused sampler border colors prevented source interning"); const auto sampler_source = fixture.program.info.samplers[0].source; Check(fixture.program.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 TestSampleAdjustSamplerScratch() { Fixture fixture(ShaderType::Pixel); const auto active = fixture.Emit( ValueOpcode::IEqual32, {fixture.Emit(ValueOpcode::LaneId), Value(0u)}); const auto lane = fixture.Emit(ValueOpcode::SelectU32, {active, Value(1u), Value(0u)}); const auto low = fixture.Emit(ValueOpcode::BitwiseAnd32, {lane, Value(0xffu)}); const auto high = fixture.Emit(ValueOpcode::BitwiseAnd32, {lane, Value(0xffu)}); const auto quads = fixture.Emit( ValueOpcode::BitwiseOr32, {low, fixture.Emit(ValueOpcode::ShiftLeftLogical32, {high, Value(8u)})}); const auto scratch = fixture.Emit(ValueOpcode::ShiftLeftLogical32, {quads, Value(12u)}); const auto word3 = fixture.Emit(ValueOpcode::BitwiseOr32, {fixture.UserData(3), scratch}); const auto image = fixture.Image({Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u)}, 0x1ec); const auto sampler = fixture.Sampler( {fixture.UserData(0), fixture.UserData(1), fixture.UserData(2), word3}, 0x1ec); MemoryInfo memory; memory.kind = ResourceKind::Image; memory.image_dimension = Decoder::ImageDimension::Dim2D; memory.image_sample_flags = Decoder::ImageSampleFlagAdjust; fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress()}, fixture.AddMemory(memory, 0x1ec)); fixture.PlanAndTrack(); const auto source = fixture.program.info.samplers[0].source; const auto stored = fixture.program.descriptor_sources[source] .dwords[3] .Resolve() .TryInstruction(); Check(stored != nullptr && stored->GetOpcode() == ValueOpcode::GetUserData, "SampleAdjust reserved scratch remained in sampler identity"); std::array user_data{4u, 1u, 2u, 0x80000abcu}; SrtRuntime runtime{.user_data = user_data}; DescriptorValue descriptor; Check(SrtWalker(fixture.program, runtime).EvaluateDescriptor(source, descriptor) && descriptor.dwords[3] == 0x80000abcu, "SampleAdjust canonicalization lost sampler border fields"); const auto CheckRejected = [](uint32_t flags, uint32_t shift, const char *message) { Fixture rejected(ShaderType::Pixel); const auto condition = rejected.Emit( ValueOpcode::IEqual32, {rejected.Emit(ValueOpcode::LaneId), Value(0u)}); const auto bit = rejected.Emit(ValueOpcode::SelectU32, {condition, Value(1u), Value(0u)}); const auto dynamic = rejected.Emit(ValueOpcode::ShiftLeftLogical32, {bit, Value(shift)}); const auto dynamic_word3 = rejected.Emit(ValueOpcode::BitwiseOr32, {rejected.UserData(3), dynamic}); const auto rejected_image = rejected.Image({Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u)}, 0x200); const auto rejected_sampler = rejected.Sampler({rejected.UserData(0), rejected.UserData(1), rejected.UserData(2), dynamic_word3}, 0x200); MemoryInfo rejected_memory; rejected_memory.kind = ResourceKind::Image; rejected_memory.image_dimension = Decoder::ImageDimension::Dim2D; rejected_memory.image_sample_flags = flags; rejected.Emit(ValueOpcode::ImageSampleRaw, {rejected_image, rejected_sampler, rejected.ImageAddress()}, rejected.AddMemory(rejected_memory, 0x200)); BuildSrtPlan(rejected.program); CheckFatal([&] { TrackResources(rejected.program); }, "not a valid runtime value", message); }; CheckRejected(0u, 12u, "ordinary sampling accepted SampleAdjust reserved scratch"); CheckRejected(Decoder::ImageSampleFlagAdjust, 30u, "SampleAdjust canonicalization discarded border-mode bits"); } void TestFmaskLoadSpecialization() { namespace Prospero = Libs::Graphics::Prospero; Fixture fixture; std::array words; for (uint32_t i = 0; i < words.size(); i++) { words[i] = fixture.UserData(i); } const auto fmask = fixture.Image(words, 4); const auto active = fixture.Emit(ValueOpcode::IEqual32, {fixture.UserData(8), Value(0u)}); MemoryInfo load; load.kind = ResourceKind::Image; load.image_dimension = Decoder::ImageDimension::Dim2D; load.image_address_components = 2; load.dmask = 1; const auto mapping = fixture.Emit( ValueOpcode::ImageRead, {fmask, fixture.ImageAddress(), active}, fixture.AddMemory(load, 4)); const auto ordinary = fixture.Image( {Value(0x2000u), Value(static_cast(Prospero::BufferFormat::k8UInt) << 20u), Value(3u | (3u << 14u)), Value(Libs::Graphics::DstSel(4, 5, 6, 7) | (static_cast(Prospero::ImageType::kColor2D) << 28u)), Value(0u), Value(0u), Value(0u), Value(0u)}, 8); const auto ordinary_flags = fixture.AddMemory(load, 8); const auto color = fixture.Emit( ValueOpcode::ImageRead, {ordinary, fixture.ImageAddress(), Value(true)}, ordinary_flags); const auto output = fixture.Buffer( {Value(0x3000u), Value(0u), Value(12u), Value(0u)}, 12); MemoryInfo store; store.kind = ResourceKind::Buffer; Value result; for (uint32_t i = 0; i < 2; i++) { const auto value = fixture.Emit( ValueOpcode::CompositeExtractU32x4, {i == 0 ? mapping : color, Value(0u)}); fixture.Emit(ValueOpcode::StoreBufferU32, {output, Value(0u), Value(i * 4u), Value(0u), value, Value(true)}, fixture.AddMemory(store, 12 + i * 4u)); if (i == 0) result = value; } fixture.PlanAndTrack(); const auto plan = ExtractResourcePlan(fixture.program); std::array user_data{ 0x303ac300u, 0xca100000u, 0x021bc3bfu, 0x91800004u, 0u, 0x00700000u, 0u, 0u}; ResourceSnapshot snapshot; ResourceSpecialization specialization; Check(MaterializeResources(plan, {.user_data = user_data}, snapshot, specialization), "FMASK resources did not materialize"); ApplyResourceSpecialization(fixture.program, specialization); RemoveIdentities(fixture.program.blocks); EliminateDeadCode(fixture.program.blocks); Check(fixture.program.info.images.size() == 1 && snapshot.images.size() == 1 && snapshot.images[0].dwords[0] == 0x2000u && ordinary.Instruction()->Flags() == 0 && fixture.program.memory_info[ordinary_flags.index].resource == 0, "FMASK removal did not preserve the remaining image and runtime descriptor"); const auto *vector = result.Instruction()->Arg(0).Resolve().TryInstruction(); Check(vector != nullptr && vector->GetOpcode() == ValueOpcode::CompositeConstructU32x4, "FMASK load did not lower to a value vector"); result = vector->Arg(0); uint32_t value = 0; Check(SrtWalker(fixture.program, {.user_data = user_data}).Evaluate(result, value) && value == 0x76543210u, "FMASK load did not return the native sample-to-fragment mapping"); user_data[8] = 1; Check(SrtWalker(fixture.program, {.user_data = user_data}).Evaluate(result, value) && value == 0u, "inactive FMASK load did not preserve the execution mask"); ShaderComputeInputInfo compute{}; CollectShaderInfo(fixture.program, {.compute = &compute}); AllocateBindings(fixture.program); const auto kind = DescriptorBindingForImage(fixture.program.info.images[0]); Check(kind.has_value() && FindBinding(fixture.program.bindings, *kind)->resources == std::vector{0}, "FMASK allocated an ordinary image descriptor"); user_data[8] = 0; user_data[1] = static_cast(Prospero::BufferFormat::k8UInt) << 20u; ResourceSpecialization rebound; Check(MaterializeResources(plan, {.user_data = user_data}, snapshot, rebound) && rebound != specialization && snapshot.images.size() == 2, "rebinding FMASK as a texture reused the metadata specialization"); } void TestDynamicStorageMipTracking() { Fixture fixture; std::array image_words; for (uint32_t index = 0; index < image_words.size(); index++) { image_words[index] = fixture.UserData(index); } const auto data = fixture.Emit(ValueOpcode::CompositeConstructU32x4, {Value(1u), Value(2u), Value(3u), Value(4u)}); const auto AddStore = [&](uint32_t pc, bool has_mip, Value lod) { const auto handle = fixture.Image(image_words, pc); const auto address = fixture.Emit( ValueOpcode::MakeImageAddress, {Value(0u), Value(0u), lod, Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u)}); MemoryInfo memory; memory.kind = ResourceKind::Image; memory.image_dimension = Decoder::ImageDimension::Dim2D; memory.image_address_components = has_mip ? 3u : 2u; memory.image_has_mip = has_mip; const auto flags = fixture.AddMemory(memory, pc); fixture.Emit(ValueOpcode::ImageWrite, {handle, address, data, Value(true)}, flags); return std::pair{handle, flags.index}; }; const auto plain = AddStore(4, false, Value(0u)); const auto mip1 = AddStore(8, true, Value(1u)); const auto mip2 = AddStore(12, true, Value(2u)); const auto dynamic = AddStore(16, true, fixture.UserData(8)); fixture.PlanAndTrack(); auto resource_plan = ExtractResourcePlan(fixture.program); const auto &images = fixture.program.info.images; Check(images.size() == 2 && images[0].mip_mode == ImageMipMode::None && images[0].mip_count == 1 && images[1].mip_mode == ImageMipMode::DynamicStorage && images[1].mip_count == 1, "storage mip writes did not share one dynamic logical resource"); Check(plain.first.Instruction()->Flags() == 0 && mip1.first.Instruction()->Flags() == 1 && mip2.first.Instruction()->Flags() == 1 && dynamic.first.Instruction()->Flags() == 1 && fixture.program.memory_info[plain.second].resource == 0 && fixture.program.memory_info[mip1.second].resource == 1 && fixture.program.memory_info[mip2.second].resource == 1 && fixture.program.memory_info[dynamic.second].resource == 1, "dynamic storage mip handles and memory metadata were not patched"); DescriptorValue descriptor{}; descriptor.dwords[0] = 0x1000u; descriptor.dwords[1] = static_cast( Libs::Graphics::Prospero::BufferFormat::k32_32_32_32Float) << 20u; descriptor.dwords[2] = 3u | (3u << 14u); descriptor.dwords[3] = Libs::Graphics::DstSel(4, 5, 6, 7) | (1u << 12u) | (3u << 16u) | (static_cast(Libs::Graphics::Prospero::ImageType::kColor2D) << 28u); descriptor.dwords[5] = 3u << 4u; descriptor.dword_count = 8; std::array user_data{}; std::copy(descriptor.dwords.begin(), descriptor.dwords.end(), user_data.begin()); user_data[8] = 2u; SrtRuntime runtime{.user_data = user_data}; ResourceSnapshot snapshot; ResourceSpecialization specialization; Check(MaterializeResources(resource_plan, runtime, snapshot, specialization), "dynamic storage resources did not materialize"); ApplyResourceSpecialization(fixture.program, specialization); Check(fixture.program.info.images[1].mip_count == 3 && snapshot.images.size() == fixture.program.info.images.size(), "base-1 through last-3 dynamic storage range was not specialized"); ShaderComputeInputInfo compute{}; CollectShaderInfo(fixture.program, {.compute = &compute}); AllocateBindings(fixture.program); const auto storage_kind = DescriptorBindingForImage(images[0]); Check(storage_kind.has_value(), "storage image has no descriptor binding"); const auto *storage_binding = FindBinding(fixture.program.bindings, *storage_kind); Check(storage_binding != nullptr && storage_binding->resources == std::vector({0, 1, 1, 1}), "dynamic storage mip descriptors were not expanded consecutively"); Fixture null_fixture; const auto null_handle = null_fixture.Image( {Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u)}); const auto null_address = null_fixture.Emit( ValueOpcode::MakeImageAddress, {Value(0u), Value(0u), null_fixture.UserData(0), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u)}); MemoryInfo null_memory; null_memory.kind = ResourceKind::Image; null_memory.image_dimension = Decoder::ImageDimension::Dim2D; null_memory.image_address_components = 3u; null_memory.image_has_mip = true; const auto null_data = null_fixture.Emit( ValueOpcode::CompositeConstructU32x4, {Value(1u), Value(2u), Value(3u), Value(4u)}); null_fixture.Emit(ValueOpcode::ImageWrite, {null_handle, null_address, null_data, Value(true)}, null_fixture.AddMemory(null_memory, 4)); null_fixture.PlanAndTrack(); auto null_plan = ExtractResourcePlan(null_fixture.program); ResourceSnapshot null_snapshot; ResourceSpecialization null_specialization; const std::array null_user_data{0u}; Check(MaterializeResources(null_plan, {.user_data = null_user_data}, null_snapshot, null_specialization), "canonical null dynamic storage image did not materialize"); ApplyResourceSpecialization(null_fixture.program, null_specialization); Check(null_fixture.program.info.images[0].mip_count == 1 && null_snapshot.images.size() == 1, "canonical null dynamic storage image did not retain one descriptor"); auto changed_user_data = user_data; changed_user_data[3] = (changed_user_data[3] & ~(0xfu << 16u)) | (2u << 16u); ResourceSnapshot changed_snapshot; ResourceSpecialization changed_specialization; Check(MaterializeResources(resource_plan, {.user_data = changed_user_data}, changed_snapshot, changed_specialization) && changed_specialization != specialization, "a changed dynamic storage mip count reused the specialization key"); changed_user_data[3] = (changed_user_data[3] & ~((0xfu << 12u) | (0xfu << 16u))) | (4u << 12u) | (3u << 16u); Check(!MaterializeResources(resource_plan, {.user_data = changed_user_data}, changed_snapshot, changed_specialization), "an inverted dynamic storage mip range was accepted"); } void TestSrtFlatteningAndRuntimeMemoization() { Fixture fixture; const auto base = fixture.Address(fixture.UserData(0), fixture.UserData(1), 4); MemoryInfo scalar; scalar.kind = ResourceKind::ScalarAddress; 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.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.uses_dma, "planning-only scalar reads leaked into resource topology"); Check(fixture.program.memory_info[0].planning_only, "canonical runtime scalar read was not marked planning-only"); std::array user_data{0x1000u, 0u}; TestMemory memory; memory.words[1] = 0xdeadbeefu; SrtRuntime runtime{.user_data = user_data, .read_memory = ReadTestMemory, .userdata = &memory}; DescriptorValue descriptor; std::vector flat; const uint32_t request = fixture.program.info.buffers[0].source; const auto refresh = [&](const ResourcePlan& plan) { SrtWalker walker(plan, runtime); return walker.EvaluateDescriptor(request, descriptor) && walker.RefreshFlatBuffer(flat); }; Check(refresh(fixture.program), "typed runtime source evaluation failed"); Check(descriptor.dwords[0] == 0xdeadbeefu && flat == std::vector{0xdeadbeefu} && memory.reads == 1, "descriptor and flat SRT evaluation did not share one memoized read"); memory.reads = 0; memory.words[1] = 0x12345678u; Check(refresh(fixture.program) && descriptor.dwords[0] == 0x12345678u && flat == std::vector{0x12345678u} && memory.reads == 1, "repeated runtime evaluation reused stale scalar memory"); memory.reads = 0; memory.fail_after = 0; Check(!refresh(fixture.program), "unavailable scalar memory was accepted"); memory.fail_after = UINT32_MAX; Check(refresh(fixture.program) && descriptor.dwords[0] == 0x12345678u && memory.reads == 1, "failed runtime evaluation left a value marked as visiting"); auto detached = ExtractResourcePlan(fixture.program); Check(refresh(detached), "detached resource plan did not evaluate"); auto moved = std::move(detached); memory.reads = 0; memory.words[1] = 0x87654321u; Check(refresh(moved) && descriptor.dwords[0] == 0x87654321u && memory.reads == 1, "moving a cached resource plan lost its evaluation state"); ShaderComputeInputInfo compute{}; CollectShaderInfo(fixture.program, {.compute = &compute}); AllocateBindings(fixture.program); Check(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::ScalarAddress; 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.srt_reads.empty() && fixture.program.dynamic_reads.size() == 1 && fixture.program.info.uses_dma, "dynamic scalar read was incorrectly flattened or lost"); std::array user_data{0x1000u, 0u, 4u}; TestMemory memory; memory.words[1] = 0xabcdef01u; SrtRuntime runtime{.user_data = user_data, .read_memory = ReadTestMemory, .userdata = &memory}; DescriptorValue value; Check(SrtWalker(fixture.program, runtime).EvaluateDescriptor(fixture.program.info.buffers[0].source, value) && value.dwords[0] == 0xabcdef01u && memory.reads == 1, "dynamic typed scalar descriptor source was not evaluated"); ShaderComputeInputInfo compute{}; CollectShaderInfo(fixture.program, {.compute = &compute}); AllocateBindings(fixture.program); Check(FindBinding(fixture.program.bindings, DescriptorBindingKind::FlattenedSrt) == nullptr && FindBinding(fixture.program.bindings, DescriptorBindingKind::BdaPagetable) != nullptr && FindBinding(fixture.program.bindings, DescriptorBindingKind::FaultBuffer) != nullptr, "dynamic scalar read received the wrong resource bindings"); Check(fixture.program.bindings.memory_offset_dword == fixture.program.bindings.user_data_registers.size() && fixture.program.bindings.memory_offset_count == 1u && fixture.program.bindings.ShaderDataDwords() == fixture.program.bindings.memory_offset_dword + 1u, "unified memory-offset layout is inconsistent"); } 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(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); BuildSrtPlan(fixture.program); CheckFatal([&] { TrackResources(fixture.program); }, "not a valid runtime value", "control-dependent descriptor phi was accepted"); Check(!fixture.program.resource_tracking_complete && fixture.program.info.buffers.empty() && fixture.program.descriptor_sources.empty(), "control-dependent descriptor phi was not rejected transactionally"); } ResourcePlan ConditionalSamplerPlan(bool diamond, bool reverse, bool reverse_phi, bool nonuniform = false, bool writable = false) { namespace CFG = Libs::Graphics::ShaderRecompiler::CFG; Fixture fixture(ShaderType::Pixel); auto *entry = fixture.block; auto *initial = diamond ? fixture.AddBlock() : entry; auto *alternate = fixture.AddBlock(); auto *merge = fixture.AddBlock(); const uint32_t alternate_id = diamond ? 2u : 1u; const uint32_t merge_id = alternate_id + 1; const uint32_t initial_target = diamond ? 1u : merge_id; entry->AddBranch(alternate); entry->AddBranch(diamond ? initial : merge); alternate->AddBranch(merge); if (diamond) { initial->AddBranch(merge); fixture.program.block_info[1].terminator = { .kind = CFG::TerminatorKind::Branch, .true_block = merge_id}; } fixture.program.block_info[0].terminator = { .kind = CFG::TerminatorKind::ConditionalBranch, .true_block = reverse ? alternate_id : initial_target, .false_block = reverse ? initial_target : alternate_id}; fixture.program.block_info[alternate_id].terminator = { .kind = CFG::TerminatorKind::Branch, .true_block = merge_id}; fixture.program.block_info[merge_id].terminator.kind = CFG::TerminatorKind::Return; const auto control = fixture.Buffer({Value(0x2000u), Value(0u), Value(200u), Value(0u)}); MemoryInfo scalar; scalar.kind = ResourceKind::ScalarBuffer; auto flag = fixture.Emit(ValueOpcode::ReadConstBuffer, {control, Value(196u)}, fixture.AddMemory(scalar, 0x498)); if (nonuniform) { flag = fixture.Emit(ValueOpcode::LaneId); } fixture.program.block_info[0].condition = fixture.Emit(ValueOpcode::SGreaterThanEqual32, {flag, Value(0u)}); if (writable) { MemoryInfo memory; memory.kind = ResourceKind::Buffer; fixture.Emit( ValueOpcode::StoreBufferU32, {control, Value(0u), Value(0u), Value(0u), Value(1u), Value(true)}, fixture.AddMemory(memory, 0x170)); } std::array sampler_words; for (uint32_t word = 0; word < sampler_words.size(); ++word) { const auto read = [&](Block *block, uint32_t address, uint32_t pc) { const auto handle = fixture.Emit(ValueOpcode::GetAddressResource, {Value(address), Value(0u)}, 0, block); MemoryInfo memory; memory.kind = ResourceKind::ScalarAddress; memory.offset = word * 4; return fixture.Emit(ValueOpcode::LoadAddressU32, {handle, Value(0u), Value(0u), Value(true)}, fixture.AddMemory(memory, pc), block); }; // PS 2190adcc312b2e6e selects SRT+448 or SRT+480 before its sample at // 0x4d4. const auto first = read(initial, 0x1000 + 448, 0x4c8); const auto second = read(alternate, 0x1000 + 480, 0x4bc); auto &phi = merge->AppendNewInst(ValueOpcode::Phi, {}, static_cast(Type::U32)); if (reverse_phi) { phi.AddPhiOperand(alternate, second); phi.AddPhiOperand(initial, first); } else { phi.AddPhiOperand(initial, first); phi.AddPhiOperand(alternate, second); } sampler_words[word] = Value(&phi); } fixture.block = merge; const auto image = fixture.Image({Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u)}); const auto address = fixture.ImageAddress(); for (uint32_t use = 0; use < 2; ++use) { const auto sampler = fixture.Sampler(sampler_words); MemoryInfo sample; sample.kind = ResourceKind::Image; sample.image_dimension = Decoder::ImageDimension::Dim2D; const auto result = fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, address}, fixture.AddMemory(sample, 0x4d4)); fixture.Emit(ValueOpcode::ReferenceU32, {fixture.Emit(ValueOpcode::CompositeExtractU32x4, {result, Value(0u)})}); } fixture.PlanAndTrack(); Check(fixture.program.value_storage.size() == 4, "repeated sampler uses retained duplicate planning selections"); Check(sampler_words[0].ResolveInstruction()->GetOpcode() == ValueOpcode::Phi, "host descriptor selection changed the GPU Phi"); EliminateDeadCode(fixture.program.blocks); ValidateProgram(fixture.program, true); return ExtractResourcePlan(fixture.program); } void TestConditionalSamplerPhi() { for (const bool diamond : {false, true}) { for (const bool reverse : {false, true}) { for (const bool reverse_phi : {false, true}) { auto plan = ConditionalSamplerPlan(diamond, reverse, reverse_phi); const auto source = plan.info.samplers[0].source; LinearTestMemory memory; for (uint32_t word = 448 / 4; word < (480 + 16) / 4; ++word) { memory.words[word] = 0x400u + word; } const SrtRuntime runtime{.read_memory = ReadLinearTestMemory, .userdata = &memory, .read_specialization_memory = ReadLinearTestMemory}; // Sampler selection compares a signed value against zero. for (const auto flag : {-1, 0, 1, INT32_MIN, INT32_MAX}) { memory.words[(0x1000 + 196) / 4] = std::bit_cast(flag); const uint32_t first = (flag < 0) != reverse ? 480 / 4 : 448 / 4; memory.fail_address = 0x1000 + (first == 448 / 4 ? 480u : 448u); DescriptorValue selected; SrtWalker clean(plan, CleanRuntime(runtime)); Check(SrtWalker(plan, runtime, {}, &clean).EvaluateDescriptor(source, selected), "conditional sampler did not survive detached plan lifetime"); for (uint32_t word = 0; word < 4; ++word) { Check(selected.dwords[word] == memory.words[first + word], "conditional sampler chose the wrong incoming descriptor"); } } DescriptorValue selected; auto no_clean_reader = runtime; no_clean_reader.read_specialization_memory = nullptr; { SrtWalker clean(plan, CleanRuntime(no_clean_reader)); Check(!SrtWalker(plan, no_clean_reader, {}, &clean).EvaluateDescriptor(source, selected), "conditional sampler used unchecked memory for its predicate"); } memory.fail_address = 0x2000 + 196; { SrtWalker clean(plan, CleanRuntime(runtime)); Check(!SrtWalker(plan, runtime, {}, &clean).EvaluateDescriptor(source, selected), "conditional sampler ignored unavailable coherent predicate memory"); } } } CheckFatal([&] { ConditionalSamplerPlan(diamond, false, false, true); }, "not a valid runtime value", "nonuniform sampler selection was accepted"); CheckFatal( [&] { ConditionalSamplerPlan(diamond, false, false, false, true); }, "not a valid runtime value", "shader-written sampler predicate was accepted"); } } void TestGuardedSamplerPhi() { namespace CFG = Libs::Graphics::ShaderRecompiler::CFG; const auto make_plan = [](bool take_true, bool reverse_phi, bool bypass, bool mismatched, bool ambiguous) { Fixture fixture(ShaderType::Pixel); auto *entry = fixture.block; auto *loaded = fixture.AddBlock(); auto *merge = fixture.AddBlock(); auto *sample = fixture.AddBlock(); auto *exit = fixture.AddBlock(); entry->AddBranch(loaded); entry->AddBranch(merge); loaded->AddBranch(merge); merge->AddBranch(sample); merge->AddBranch(exit); sample->AddBranch(exit); if (bypass) exit->AddBranch(sample); const auto lane = fixture.Emit(ValueOpcode::LaneId); const auto predicate = fixture.Emit(ValueOpcode::IEqual32, {lane, Value(0u)}); fixture.program.block_info[0].condition = predicate; fixture.program.block_info[0].terminator = { .kind = CFG::TerminatorKind::ConditionalBranch, .true_block = 1u, .false_block = 2u}; fixture.program.block_info[1].terminator = { .kind = CFG::TerminatorKind::Branch, .true_block = 2u}; auto &guard = merge->AppendNewInst(ValueOpcode::Phi, {}, static_cast(Type::U1)); guard.AddPhiOperand(entry, ambiguous ? predicate : Value(!take_true)); guard.AddPhiOperand(mismatched ? sample : loaded, predicate); fixture.program.block_info[2].condition = Value(&guard); fixture.program.block_info[2].terminator = { .kind = CFG::TerminatorKind::ConditionalBranch, .true_block = take_true ? 3u : 4u, .false_block = take_true ? 4u : 3u}; fixture.program.block_info[3].terminator = { .kind = CFG::TerminatorKind::Branch, .true_block = 4u}; fixture.program.block_info[4].terminator = { .kind = bypass ? CFG::TerminatorKind::Branch : CFG::TerminatorKind::Return, .true_block = 3u}; std::array words; for (uint32_t word = 0; word < words.size(); ++word) { // An arbitrary excluded value proves this is control-flow reasoning, not null filtering. const auto first = Value(0xbad000u + word); const auto second = fixture.UserData(word); auto &phi = merge->AppendNewInst(ValueOpcode::Phi, {}, static_cast(Type::U32)); phi.AddPhiOperand(reverse_phi ? loaded : entry, reverse_phi ? second : first); phi.AddPhiOperand(reverse_phi ? entry : loaded, reverse_phi ? first : second); words[word] = Value(&phi); } fixture.block = sample; const auto image = fixture.Image({Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u)}); const auto sampler = fixture.Sampler(words); MemoryInfo memory; memory.kind = ResourceKind::Image; memory.image_dimension = Decoder::ImageDimension::Dim2D; fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress()}, fixture.AddMemory(memory, 0x2a0)); fixture.PlanAndTrack(); Check(words[0].ResolveInstruction()->GetOpcode() == ValueOpcode::Phi, "guarded host descriptor selection rewrote the GPU Phi"); return ExtractResourcePlan(fixture.program); }; for (const bool take_true : {false, true}) { for (const bool reverse_phi : {false, true}) { auto plan = make_plan(take_true, reverse_phi, false, false, false); const std::array user_data{0x444u, 0x555u, 0x666u, 0x777u}; DescriptorValue selected; Check(SrtWalker(plan, {.user_data = user_data}).EvaluateDescriptor( plan.info.samplers[0].source, selected) && std::equal(user_data.begin(), user_data.end(), selected.dwords.begin()), "guarded sampler did not match descriptor and predicate predecessors"); } } CheckFatal([&] { make_plan(true, false, true, false, false); }, "not a valid runtime value", "sampler guard accepted an unguarded path"); CheckFatal([&] { make_plan(true, false, false, true, false); }, "not a valid runtime value", "sampler guard ignored predecessor identity"); CheckFatal([&] { make_plan(true, false, false, false, true); }, "not a valid runtime value", "sampler guard discarded a reachable alternative"); } 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(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); BuildSrtPlan(fixture.program); } 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(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 user_data{0x12345678u}; SrtRuntime runtime{.user_data = user_data}; DescriptorValue descriptor; Check(SrtWalker(fixture.program, runtime).EvaluateDescriptor(fixture.program.info.buffers[0].source, descriptor) && descriptor.dwords[0] == user_data[0], "loop-invariant descriptor phi was not evaluated through typed SSA"); } void TestDmaAddressMaterialization() { Fixture fixture; const auto based = fixture.Address(fixture.UserData(0), fixture.UserData(1), 4); MemoryInfo global; global.kind = ResourceKind::Global; global.offset = static_cast(-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(); auto resource_plan = ExtractResourcePlan(fixture.program); Check(fixture.program.info.uses_dma, "typed address operations did not enable DMA"); std::array user_data{0x2008u, 0u}; SrtRuntime runtime{.user_data = user_data}; ResourceSnapshot snapshot; ResourceSpecialization specialization; Check(MaterializeResources(resource_plan, runtime, snapshot, specialization), "DMA shader resources did not materialize"); ApplyResourceSpecialization(fixture.program, specialization); } void TestDynamicFlatAddressesUseDma() { Fixture fixture; const auto low_root = fixture.UserData(0); const auto high_root = fixture.UserData(1); const auto active = fixture.Emit(ValueOpcode::INotEqual32, {fixture.UserData(2), Value(0u)}); const auto inactive_low = fixture.Emit(ValueOpcode::UndefU32); const auto inactive_high = fixture.Emit(ValueOpcode::UndefU32); const auto low = fixture.Emit(ValueOpcode::SelectU32, {active, low_root, inactive_low}); const auto high = fixture.Emit(ValueOpcode::SelectU32, {active, high_root, inactive_high}); const auto address = fixture.Address(low, high, 0xa4); MemoryInfo flat; flat.kind = ResourceKind::Flat; flat.address_is_full = true; fixture.Emit(ValueOpcode::LoadAddressU8, {address, low, high, active}, fixture.AddMemory(flat, 0xa4)); fixture.PlanAndTrack(); auto resource_plan = ExtractResourcePlan(fixture.program); Check(fixture.program.info.uses_dma, "exec-masked FLAT address did not enable DMA"); std::array user_data{0x23456780u, 1u, 1u}; SrtRuntime runtime{.user_data = user_data}; ResourceSnapshot snapshot; ResourceSpecialization specialization; Check(MaterializeResources(resource_plan, runtime, snapshot, specialization), "exec-masked FLAT shader resources did not materialize"); Fixture mismatch; const auto mismatch_active = mismatch.Emit(ValueOpcode::INotEqual32, {mismatch.UserData(2), Value(0u)}); const auto other_active = mismatch.Emit(ValueOpcode::LogicalNot, {mismatch_active}); const auto mismatch_low = mismatch.Emit( ValueOpcode::SelectU32, {mismatch_active, mismatch.UserData(0), mismatch.Emit(ValueOpcode::UndefU32)}); const auto mismatch_high = mismatch.Emit( ValueOpcode::SelectU32, {mismatch_active, mismatch.UserData(1), mismatch.Emit(ValueOpcode::UndefU32)}); const auto mismatch_address = mismatch.Address(mismatch_low, mismatch_high, 0xa4); mismatch.Emit(ValueOpcode::LoadAddressU8, {mismatch_address, mismatch_low, mismatch_high, other_active}, mismatch.AddMemory(flat, 0xa4)); mismatch.PlanAndTrack(); Check(mismatch.program.info.uses_dma, "dynamic FLAT address did not enable DMA"); } void TestBufferSwizzleSpecialization() { Fixture fixture; const auto handle = fixture.Buffer({fixture.UserData(0), fixture.UserData(1), fixture.UserData(2), fixture.UserData(3)}, 4); MemoryInfo memory; memory.kind = ResourceKind::Buffer; memory.formatted = true; fixture.Emit(ValueOpcode::LoadBufferU32, {handle, Value(0u), Value(0u), Value(0u), Value(true)}, fixture.AddMemory(memory, 4)); fixture.PlanAndTrack(); auto resource_plan = ExtractResourcePlan(fixture.program); constexpr auto swizzle = Libs::Graphics::DstSel(4, 5, 0, 1); std::array user_data{ 0, 16u << 16u, 1, swizzle | (static_cast( Libs::Graphics::Prospero::BufferFormat::k32_32Float) << 12u) | (1u << 24u)}; SrtRuntime runtime{.user_data = user_data}; ResourceSnapshot snapshot; ResourceSpecialization specialization; Check(MaterializeResources(resource_plan, runtime, snapshot, specialization), "buffer resources did not materialize"); ApplyResourceSpecialization(fixture.program, specialization); Check(fixture.program.info.buffers[0].descriptor_swizzle == swizzle && specialization.buffers[0].descriptor_swizzle == swizzle, "buffer destination selectors were not specialized"); user_data[3] ^= 1u << 9u; ResourceSnapshot changed_snapshot; ResourceSpecialization changed_specialization; Check(MaterializeResources(resource_plan, runtime, changed_snapshot, changed_specialization) && changed_specialization != specialization, "buffer swizzle change did not select a new specialization key"); } enum class ConditionalBufferUse { Optional, Shared, Loop, Writable }; ResourcePlan ConditionalBufferPlan(ConditionalBufferUse use) { namespace CFG = Libs::Graphics::ShaderRecompiler::CFG; Fixture fixture; auto *entry = fixture.block; auto *optional = fixture.AddBlock(); auto *done = fixture.AddBlock(); auto *condition_block = entry; uint32_t condition_index = 0; fixture.program.block_info[0].id = 11; fixture.program.block_info[1].id = 27; fixture.program.block_info[2].id = 42; if (use == ConditionalBufferUse::Loop) { condition_block = fixture.AddBlock(); condition_index = 3; fixture.program.block_info[3].id = 55; fixture.program.block_info[0].terminator = { .kind = CFG::TerminatorKind::Branch, .true_block = 55}; entry->AddBranch(condition_block); } condition_block->AddBranch(optional); condition_block->AddBranch(done); optional->AddBranch(use == ConditionalBufferUse::Loop ? condition_block : done); fixture.program.block_info[condition_index].terminator = { .kind = CFG::TerminatorKind::ConditionalBranch, .true_block = 27, .false_block = 42}; fixture.program.block_info[1].terminator = { .kind = CFG::TerminatorKind::Branch, .true_block = use == ConditionalBufferUse::Loop ? 55u : 42u}; const auto control = fixture.Buffer( {fixture.UserData(0), fixture.UserData(1), fixture.UserData(2), fixture.UserData(3)}, 4); MemoryInfo scalar; scalar.kind = ResourceKind::ScalarBuffer; auto flag = fixture.Emit(ValueOpcode::ReadConstBuffer, {control, Value(0u)}, fixture.AddMemory(scalar, 4)); if (use == ConditionalBufferUse::Loop) { auto &phi = condition_block->AppendNewInst(ValueOpcode::Phi, {}, static_cast(Type::U32)); phi.AddPhiOperand(entry, flag); phi.AddPhiOperand(optional, Value(1u)); flag = Value(&phi); } fixture.program.block_info[condition_index].condition = fixture.Emit(ValueOpcode::INotEqual32, {flag, Value(0u)}, 0, condition_block); const auto payload = fixture.Buffer( {fixture.UserData(4), fixture.UserData(5), fixture.UserData(6), fixture.UserData(7)}, 8); MemoryInfo vector; vector.kind = ResourceKind::Buffer; const auto load = [&](Block *block) { fixture.Emit(ValueOpcode::LoadBufferU32, {payload, Value(0u), Value(0u), Value(0u), Value(true)}, fixture.AddMemory(vector, 8), block); }; load(optional); if (use == ConditionalBufferUse::Shared) { load(done); } if (use == ConditionalBufferUse::Writable) { fixture.Emit(ValueOpcode::StoreBufferU32, {control, Value(0u), Value(0u), Value(0u), Value(1u), Value(true)}, fixture.AddMemory(vector, 12)); } fixture.PlanAndTrack(); return ExtractResourcePlan(fixture.program); } void TestConditionalBufferMaterialization() { auto plan = ConditionalBufferPlan(ConditionalBufferUse::Optional); // GTA III leaves packet words in s[12:15] when its scalar control word is zero. std::array user_data{ 0x1000, 16u << 16u, 1, 0x4dfac, 0xc0107600, 0x8c, 0x97730000, 0x100020}; TestMemory memory; SrtRuntime runtime{.user_data = user_data, .userdata = &memory, .read_specialization_memory = ReadTestMemory}; ResourceSnapshot snapshot; ResourceSpecialization specialization; Check(MaterializeResources(plan, runtime, snapshot, specialization) && snapshot.buffers.size() == 2 && snapshot.buffers[1].dword_count == 4 && snapshot.buffers[1].dwords == std::array{}, "untaken scalar branch materialized stale buffer words"); Check(snapshot.user_data == std::vector(user_data.begin(), user_data.end()), "resource reachability changed native shader user data"); runtime.user_data = std::span(user_data).first(4); Check(MaterializeResources(plan, runtime, snapshot, specialization), "untaken branch evaluated its unavailable descriptor"); memory.words[0] = 1; Check(!MaterializeResources(plan, runtime, snapshot, specialization), "taken branch accepted an unavailable descriptor"); runtime.user_data = user_data; const auto CheckActive = [&] { Check(MaterializeResources(plan, runtime, snapshot, specialization) && snapshot.buffers.size() == 2 && std::equal(user_data.begin() + 4, user_data.end(), snapshot.buffers[1].dwords.begin()), "potentially executed buffer descriptor was discarded"); }; CheckActive(); memory.words[0] = 0; memory.fail_after = memory.reads; CheckActive(); runtime.read_specialization_memory = nullptr; CheckActive(); } void TestConservativeBufferReachability() { std::array user_data{ 0x1000, 16u << 16u, 1, 0x4dfac, 0x2000, 16u << 16u, 1, 0x4dfac}; TestMemory memory; const SrtRuntime runtime{.user_data = user_data, .userdata = &memory, .read_specialization_memory = ReadTestMemory}; for (const auto use : {ConditionalBufferUse::Shared, ConditionalBufferUse::Loop, ConditionalBufferUse::Writable}) { auto plan = ConditionalBufferPlan(use); ResourceSnapshot snapshot; ResourceSpecialization specialization; Check(MaterializeResources(plan, runtime, snapshot, specialization) && snapshot.buffers.size() == 2 && std::equal(user_data.begin() + 4, user_data.end(), snapshot.buffers[1].dwords.begin()), "shared, loop-dependent, or writable-alias resource was pruned"); } } void TestConditionalIndirectImageMaterialization() { namespace CFG = Libs::Graphics::ShaderRecompiler::CFG; auto fixture = MakeIndirectImageFixture(false); auto *body = fixture->block; auto *entry = fixture->AddBlock(); auto *done = fixture->AddBlock(); entry->AddBranch(body); entry->AddBranch(done); body->AddBranch(done); const auto flag = fixture->Emit(ValueOpcode::GetUserData, {Value(static_cast(8))}, 0, entry); fixture->program.block_info[1].condition = fixture->Emit( ValueOpcode::INotEqual32, {flag, Value(0u)}, 0, entry); fixture->program.block_info[1].terminator = { .kind = CFG::TerminatorKind::ConditionalBranch, .true_block = 0, .false_block = 2}; fixture->program.block_info[0].terminator = { .kind = CFG::TerminatorKind::Branch, .true_block = 2}; std::swap(fixture->program.blocks[0], fixture->program.blocks[1]); std::swap(fixture->program.block_info[0], fixture->program.block_info[1]); fixture->PlanAndTrack(); auto plan = ExtractResourcePlan(fixture->program); std::array user_data{ 0x1000, 224u << 16u, 2, 0, 0x2000, 16u << 16u, 4, 0, 0}; uint32_t reads = 0; const SrtRuntime runtime{ .user_data = user_data, .userdata = &reads, .read_specialization_memory = [](void *data, uint64_t, std::span) { ++*static_cast(data); return false; }}; ResourceSnapshot snapshot; ResourceSpecialization specialization; Check(MaterializeResources(plan, runtime, snapshot, specialization) && reads == 0 && snapshot.images.size() == 1 && snapshot.images[0].dwords == std::array{}, "untaken indirect image branch probed its descriptor table"); user_data[8] = 1; Check(!MaterializeResources(plan, runtime, snapshot, specialization) && reads != 0, "taken indirect image branch did not require its descriptor table"); } 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(StageInputKind::GlobalInvocationId)), Value(2u)}); fixture.Emit(ValueOpcode::BitwiseXor32, {Value(1u), Value(2u)}); MemoryInfo gds; gds.kind = ResourceKind::Gds; fixture.Emit(ValueOpcode::WriteSharedU32, {Value(0u), Value(1u), Value(true)}, fixture.AddMemory(gds, 8)); fixture.PlanAndTrack(); ShaderComputeInputInfo compute{}; compute.dispatch_thread_dimensions = true; CollectShaderInfo(fixture.program, {.compute = &compute}); 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"); AllocateBindings(fixture.program); Check(FindBinding(fixture.program.bindings, DescriptorBindingKind::Buffers) != nullptr && FindBinding(fixture.program.bindings, DescriptorBindingKind::Gds) != nullptr && FindBinding(fixture.program.bindings, DescriptorBindingKind::ShaderData) == nullptr && fixture.program.bindings.UsesPushData(), "typed resources were not assigned native bindings"); Check(NativeBinding(ShaderType::Compute, DescriptorBindingKind::Buffers) == static_cast(DescriptorBindingKind::Buffers) && NativeBinding(ShaderType::Vertex, DescriptorBindingKind::Buffers) == static_cast(DescriptorBindingKind::Buffers) && NativeBinding(ShaderType::Pixel, DescriptorBindingKind::Buffers) == static_cast(DescriptorBindingKind::Count) + static_cast(DescriptorBindingKind::Buffers), "fixed stage binding ranges are inconsistent"); Check(fixture.program.bindings.user_data_registers == std::vector({3u, 4u}), "binding layout did not collect live typed user-data values"); } void TestImageBindingAbi() { using NumericClass = Libs::Graphics::Prospero::TextureNumericClass; Check(ImageBindingCount == 43u && static_cast(DescriptorBindingKind::Buffers) == 0u && static_cast(DescriptorBindingKind::Samplers) == 44u && static_cast(DescriptorBindingKind::Gds) == 45u && static_cast(DescriptorBindingKind::BdaPagetable) == 46u && static_cast(DescriptorBindingKind::FaultBuffer) == 47u && static_cast(DescriptorBindingKind::FlattenedSrt) == 48u && static_cast(DescriptorBindingKind::ShaderData) == 49u && static_cast(DescriptorBindingKind::Count) == 50u, "native descriptor binding anchors changed"); const std::array sampled_dimensions{ Decoder::ImageDimension::Dim1D, Decoder::ImageDimension::Dim1DArray, Decoder::ImageDimension::Dim2D, Decoder::ImageDimension::Dim2DArray, Decoder::ImageDimension::Dim2DMsaa, Decoder::ImageDimension::Dim2DMsaaArray, Decoder::ImageDimension::Dim3D, }; const std::array storage_dimensions{ Decoder::ImageDimension::Dim1D, Decoder::ImageDimension::Dim1DArray, Decoder::ImageDimension::Dim2D, Decoder::ImageDimension::Dim2DArray, Decoder::ImageDimension::Dim3D, }; const std::array sampled_classes{NumericClass::Float, NumericClass::Uint, NumericClass::Sint}; const std::array storage_classes{NumericClass::Float, NumericClass::Uint}; uint32_t index = 0; const auto CheckBinding = [&](ImageResourceClass resource_class, NumericClass numeric_class, Decoder::ImageDimension dimension, bool atomic, bool comparison = false) { ImageResource image; image.resource_class = resource_class; image.numeric_class = numeric_class; image.dimension = dimension; image.atomic = atomic; image.depth_compare = comparison; const auto kind = DescriptorBindingForImage(image); Check(kind.has_value() && static_cast(*kind) == FirstImageBinding + index && ImageBindingIndex(*kind) == index && ImageBindingResourceClass(*kind) == resource_class && NativeBinding(ShaderType::Compute, *kind) == FirstImageBinding + index && NativeBinding(ShaderType::Pixel, *kind) == static_cast(DescriptorBindingKind::Count) + FirstImageBinding + index, "generated image descriptor binding changed ABI"); index++; }; for (const auto numeric_class : sampled_classes) { for (const auto dimension : sampled_dimensions) { CheckBinding(ImageResourceClass::Sampled, numeric_class, dimension, false); } } for (const auto dimension : sampled_dimensions) { CheckBinding(ImageResourceClass::Sampled, NumericClass::Float, dimension, false, true); } for (const auto numeric_class : storage_classes) { for (const auto dimension : storage_dimensions) { CheckBinding(ImageResourceClass::Storage, numeric_class, dimension, false); } } for (const auto dimension : storage_dimensions) { CheckBinding(ImageResourceClass::Storage, NumericClass::Uint, dimension, true); } Check(index == ImageBindingCount, "image descriptor ABI case count changed"); const auto Invalid = [](ImageResource image) { return !DescriptorBindingForImage(image).has_value(); }; ImageResource image; Check(Invalid(image), "untyped image received a descriptor binding"); image.resource_class = ImageResourceClass::Sampled; image.numeric_class = NumericClass::Float; image.dimension = Decoder::ImageDimension::Unknown; Check(Invalid(image), "unknown sampled dimension received a descriptor binding"); image.dimension = Decoder::ImageDimension::Dim2D; image.numeric_class = NumericClass::Unsupported; Check(Invalid(image), "unsupported sampled class received a descriptor binding"); image.numeric_class = NumericClass::Uint; image.depth_compare = true; Check(Invalid(image), "integer comparison image received a descriptor binding"); image.depth_compare = false; image.numeric_class = static_cast(UINT32_MAX); Check(Invalid(image), "invalid sampled class received a descriptor binding"); image.numeric_class = NumericClass::Float; image.dimension = static_cast(UINT32_MAX); Check(Invalid(image), "invalid sampled dimension received a descriptor binding"); image.dimension = Decoder::ImageDimension::Dim2D; image.atomic = true; Check(Invalid(image), "atomic sampled image received a descriptor binding"); image.resource_class = ImageResourceClass::Storage; image.atomic = false; image.numeric_class = NumericClass::Sint; Check(Invalid(image), "signed storage image received a descriptor binding"); image.numeric_class = NumericClass::Float; image.dimension = Decoder::ImageDimension::Dim2DMsaa; Check(Invalid(image), "multisampled storage image received a descriptor binding"); image.dimension = Decoder::ImageDimension::Dim2D; image.atomic = true; Check(Invalid(image), "float atomic image received a descriptor binding"); } void TestGraphicsPushConstantLayout() { const auto AddUserData = [](Fixture &fixture, uint32_t count) { for (uint32_t index = 0; index < count; index++) { fixture.Emit(ValueOpcode::ReferenceU32, {fixture.UserData(index)}); } fixture.program.shader_info_complete = true; }; uint32_t cursor = 0; Fixture pixel(ShaderType::Pixel); AddUserData(pixel, 4); AllocateBindings(pixel.program, cursor); Check( pixel.program.bindings.UsesPushData() && pixel.program.bindings.push_data_start_dword == 0 && FindBinding(pixel.program.bindings, DescriptorBindingKind::ShaderData) == nullptr, "pixel shader did not start the shared push-data block"); pixel.program.bindings.AdvancePushData(cursor); Fixture vertex(ShaderType::Vertex); AddUserData(vertex, 9); AllocateBindings(vertex.program, cursor); Check(vertex.program.bindings.UsesPushData() && vertex.program.bindings.push_data_start_dword == 4, "vertex shader did not follow pixel data in the shared push-data block"); vertex.program.bindings.AdvancePushData(cursor); Check(cursor == 13, "graphics push-data cursor advanced incorrectly"); Fixture edge(ShaderType::Pixel); AddUserData(edge, NativePushConstantSize / sizeof(uint32_t)); AllocateBindings(edge.program); Check(edge.program.bindings.UsesPushData() && FindBinding(edge.program.bindings, DescriptorBindingKind::ShaderData) == nullptr, "the full shared push-data block did not fit"); Fixture spill(ShaderType::Pixel); AddUserData(spill, 20); AllocateBindings(spill.program, cursor); Check( !spill.program.bindings.UsesPushData() && spill.program.bindings.push_data_start_dword == PushData::NoStart && FindBinding(spill.program.bindings, DescriptorBindingKind::ShaderData) != nullptr, "a stage that exceeded the remaining shared push data did not spill to storage"); const auto spill_layout = spill.program.bindings; spill.program.bindings.AdvancePushData(cursor); Check(cursor == 13, "a spilled stage consumed shared push-data space"); Fixture repeated_spill(ShaderType::Pixel); AddUserData(repeated_spill, 20); AllocateBindings(repeated_spill.program, 20); Check(repeated_spill.program.bindings == spill_layout, "storage fallback retained an irrelevant attempted push-data position"); } 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)); } BuildSrtPlan(fixture.program); CheckFatal([&] { TrackResources(fixture.program); }, "buffer resource limit exceeded", "resource-limit failure was not reported"); Check(!fixture.program.resource_tracking_complete && fixture.program.info.buffers.empty() && fixture.program.descriptor_sources.empty(), "resource-limit failure partially mutated typed resource state"); } void TestMalformedMemoryKindsRejected() { { Fixture fixture; const auto address = fixture.Address(Value(0u), Value(0u), 4); MemoryInfo memory; memory.kind = ResourceKind::Buffer; fixture.Emit(ValueOpcode::StoreAddressU32, {address, Value(0u), Value(0u), Value(1u), Value(true)}, fixture.AddMemory(memory, 4)); BuildSrtPlan(fixture.program); CheckFatal( [&] { TrackResources(fixture.program); }, "address operation has invalid resource kind", "resource tracking accepted an address opcode with buffer metadata"); } { Fixture fixture; const auto image = fixture.Image({Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u)}, 8); MemoryInfo memory; memory.kind = ResourceKind::Flat; fixture.Emit(ValueOpcode::ImageRead, {image, fixture.ImageAddress(), Value(true)}, fixture.AddMemory(memory, 8)); BuildSrtPlan(fixture.program); CheckFatal( [&] { TrackResources(fixture.program); }, "image operation has invalid resource kind", "resource tracking accepted an image opcode with address metadata"); } } } // 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("compute buffer fill", TestComputeBufferFill); Run("scalar/vector alias", TestScalarAndVectorBufferAlias); Run("runtime unsigned min", TestRuntimeUnsignedMinDescriptor); Run("images and samplers", TestImagesSamplersAndAliases); Run("SampleAdjust sampler scratch", TestSampleAdjustSamplerScratch); Run("FMASK load specialization", TestFmaskLoadSpecialization); Run("dynamic storage mips", TestDynamicStorageMipTracking); Run("invariant indirect images", TestInvariantIndirectImageMaterialization); Run("guarded direct image table", TestGuardedDirectImageTable); Run("image descriptor fields", TestImageDescriptorFields); Run("draw-uniform scalar image", TestUniformScalarBufferImage); Run("SRT runtime", TestSrtFlatteningAndRuntimeMemoization); Run("dynamic SRT", TestDynamicSrtReadRemainsExplicit); Run("phi validation", TestPhiValidation); Run("conditional sampler phi", TestConditionalSamplerPhi); Run("guarded sampler phi", TestGuardedSamplerPhi); Run("runtime-rooted loop", TestLoopCycleEnteredThroughRuntimeValue); Run("invariant loop phi", TestInvariantLoopPhi); Run("DMA address materialization", TestDmaAddressMaterialization); Run("dynamic FLAT address", TestDynamicFlatAddressesUseDma); Run("buffer swizzle specialization", TestBufferSwizzleSpecialization); Run("conditional buffer materialization", TestConditionalBufferMaterialization); Run("conservative buffer reachability", TestConservativeBufferReachability); Run("conditional indirect image", TestConditionalIndirectImageMaterialization); Run("shader info and bindings", TestShaderInfoAndBindingLayout); Run("image binding ABI", TestImageBindingAbi); Run("graphics push constants", TestGraphicsPushConstantLayout); Run("resource limit", TestResourceLimitIsTransactional); Run("malformed memory kinds", TestMalformedMemoryKindsRejected); } 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 focused build manifest. namespace Common { int DbgExitHandler(const char *, int, std::string_view text) { throw std::runtime_error(std::string(text)); } int DbgExitHandler(const char *, int, fmt::text_style, std::string_view text) { throw std::runtime_error(std::string(text)); } 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)); } int DbgNotImplementedHandler(const char *expression, const char *file, int line) { throw std::runtime_error(std::string("typed IR not implemented: ") + 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/Program.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/passes/DeadCodeElimination.cpp"