shader: walk every resource plan with the x86 SRT walker

This commit is contained in:
nmzik
2026-10-24 02:57:24 +02:00
parent bdf48dcf3b
commit d7856753e0
12 changed files with 1782 additions and 1701 deletions
+221 -226
View File
@@ -1,5 +1,6 @@
#include "graphics/shader/recompiler/ir/ShaderIR.h"
#include "graphics/shader/recompiler/ir/passes/ResourceMaterialization.h"
#include "SrtWalkerTestMemory.h"
#include <bit>
#include <cstdio>
@@ -7,12 +8,6 @@
#include <limits>
#include <memory>
#if defined(__linux__)
#include <csignal>
#include <sys/mman.h>
#include <ucontext.h>
#endif
namespace {
void Check(bool value, const char *text) {
@@ -38,6 +33,24 @@ AddValueBlock(Libs::Graphics::ShaderRecompiler::IR::Program &program) {
return *result;
}
// Lists one extra source per block and walks the plan: each source's activity is its block's reachability.
std::vector<uint8_t> ReachedBlocks(Libs::Graphics::ShaderRecompiler::IR::ResourcePlan &plan,
const Libs::Graphics::ShaderRecompiler::IR::SrtRuntime &runtime) {
using namespace Libs::Graphics::ShaderRecompiler::IR;
const auto first = static_cast<uint32_t>(plan.descriptor_sources.size());
for (uint32_t block = 0; block < plan.control_flow.size(); ++block) {
plan.descriptor_sources.emplace_back();
plan.control_flow[block].sources.push_back(first + block);
}
CompileSrtWalker(plan);
std::vector<uint32_t> flat;
Check(WalkSrt(plan, runtime, flat, nullptr), "reachability walk failed");
std::vector<uint8_t> reached;
for (uint32_t block = 0; block < plan.control_flow.size(); ++block)
reached.push_back(WalkedActive(plan, first + block));
return reached;
}
Libs::Graphics::ShaderRecompiler::IR::Program SrtProgram(uint64_t address) {
using namespace Libs::Graphics::ShaderRecompiler::IR;
Program program;
@@ -155,7 +168,7 @@ Libs::Graphics::ShaderRecompiler::IR::Program MixedSamplerProgram() {
return program;
}
void TestMappedSrtUsesDirectReaderByDefault() {
void TestWalkerLoadsBypassSpecializationReader() {
using namespace Libs::Graphics::ShaderRecompiler::IR;
const uint32_t dword = 0x12345678;
auto plan = ExtractResourcePlan(SrtProgram(reinterpret_cast<uint64_t>(&dword)));
@@ -168,10 +181,10 @@ void TestMappedSrtUsesDirectReaderByDefault() {
Check(MaterializeResources(plan, runtime, snapshot, specialization),
"mapped SRT stage materialization failed");
Check(specialization_reads == 0,
"ordinary SRT read used the specialization reader");
"walker load used the specialization reader");
Check(snapshot.flattened_srt.size() == 1 &&
snapshot.flattened_srt[0] == dword,
"cache rematerialization did not use the direct reader by default");
"walker did not load the mapped SRT DWORD");
}
void TestNativeNestedSrtRefresh() {
@@ -215,17 +228,6 @@ void TestNativeNestedSrtRefresh() {
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
const SrtRuntime runtime{.user_data = user_data};
{
SrtWalker walker(plan, runtime);
Check(walker.RefreshFlatBuffer(snapshot.flattened_srt),
"native SRT refresh failed");
const auto loaded = first[1];
first[1] ^= 0xffffffffu;
uint32_t value = 0;
Check(walker.Evaluate(plan.srt_reads[0].value, value) && value == loaded,
"descriptor evaluation reloaded a value already copied by the native walker");
first[1] = loaded;
}
for (const auto *data : {&first, &second}) {
table = pointer_words(data->data());
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
@@ -233,32 +235,20 @@ void TestNativeNestedSrtRefresh() {
std::vector<uint32_t>{(*data)[1], table[0], table[1], (*data)[2]},
"native SRT refresh lost pointer masking, alignment, or current values");
}
#if defined(__linux__)
struct sigaction action {}, previous {};
action.sa_sigaction = +[](int, siginfo_t *, void *context) {
auto &registers = static_cast<ucontext_t *>(context)->uc_mcontext.gregs;
const auto length = SrtWalkerLoadLength(static_cast<uint64_t>(registers[REG_RIP]));
if (length == 0) std::abort();
registers[REG_RAX] = 0;
registers[REG_RIP] += length;
};
action.sa_flags = SA_SIGINFO;
auto *page = mmap(nullptr, 4096, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
Check(page != MAP_FAILED && sigaction(SIGSEGV, &action, &previous) == 0 &&
MaterializeResources(plan, {.user_data = pointer_words(page)}, snapshot, specialization) &&
snapshot.flattened_srt == std::vector<uint32_t>(4, 0u),
"faulting walker loads of a table and a null pointer did not read zero");
sigaction(SIGSEGV, &previous, nullptr);
munmap(page, 4096);
#endif
{
// Unmapped tables and the null pointer they yield read as zero.
FakeGuestMemory unmapped;
const std::array<uint32_t, 2> unmapped_table{0x1000u, 0u};
Check(MaterializeResources(plan, {.user_data = unmapped_table}, snapshot, specialization) &&
snapshot.flattened_srt == std::vector<uint32_t>(4, 0u),
"faulting walker loads of a table and a null pointer did not read zero");
}
Check(!MaterializeResources(plan, {.user_data = std::span(user_data).first(1)},
snapshot, specialization),
"native SRT walk accepted truncated user data");
const std::array<uint32_t, 2> fake_user_data{0x1003u, 0xabcd0000u};
uint32_t reads = 0;
const SrtRuntime callback_runtime{
.user_data = fake_user_data,
.read_memory = +[](void *data, uint64_t address, std::span<uint32_t> words) {
FakeGuestMemory fake(+[](void *data, uint64_t address, std::span<uint32_t> words) {
++*static_cast<uint32_t *>(data);
switch (address) {
case 0x1000u: words[0] = 0x2003u; break;
@@ -268,12 +258,11 @@ void TestNativeNestedSrtRefresh() {
default: return false;
}
return true;
},
.userdata = &reads};
Check(MaterializeResources(plan, callback_runtime, snapshot, specialization) && reads == 4 &&
}, &reads);
Check(MaterializeResources(plan, {.user_data = fake_user_data}, snapshot, specialization) && reads == 4 &&
snapshot.flattened_srt ==
std::vector<uint32_t>{0x1234u, 0x2003u, 0xabcd0000u, 0x5678u},
"native-eligible SRT bypassed its callback or repeated nested pointer reads");
"native SRT walk repeated nested pointer reads");
leaf->SetArg(0, root);
leaf->SetArg(1, Value(0u));
program.srt_reads.resize(1);
@@ -290,17 +279,6 @@ void TestNativeNestedSrtRefresh() {
Check(!MaterializeResources(bounded, {.user_data = base}, snapshot, specialization),
"native SRT accepted address underflow or 48-bit overflow");
}
high.Instruction()->SetArg(0, Value(ScalarReg(6)));
Check(ExtractResourcePlan(program).srt_code == nullptr,
"native SRT accepted nonadjacent user-data pointer words");
high.Instruction()->SetArg(0, Value(ScalarReg(5)));
program.memory_info[0].offset = 0;
program.srt_reads.push_back({pointer_low, 1});
program.srt_reads.push_back({pointer_high, 2});
leaf->SetArg(0, emit(ValueOpcode::GetAddressResource, {pointer_low, pointer_high}));
pointer_high.Instruction()->SetArg(1, Value(8u));
Check(ExtractResourcePlan(program).srt_code == nullptr,
"native SRT accepted nonadjacent nested pointer words");
}
void TestIntegerRuntimeValueFollowsSrtReads() {
@@ -352,7 +330,7 @@ void TestIntegerRuntimeValueFollowsSrtReads() {
"cyclic SRT read-first-lane dependency was accepted");
}
void TestSrtAliasesRetainReadPolicy() {
void TestSrtAliasesShareReads() {
using namespace Libs::Graphics::ShaderRecompiler::IR;
auto program = SrtProgram(0x1000u);
auto &block = *program.blocks[0];
@@ -386,64 +364,27 @@ void TestSrtAliasesRetainReadPolicy() {
inst.GetOpcode() != ValueOpcode::GetSrtResource,
"retained plan still contains flattened-read aliases");
}
Check(plan.descriptor_sources[0].dwords[0] == plan.srt_reads[0].value &&
plan.srt_reads[0].value.Instruction()->Flags<SrtReadFlags>().clean == 1u &&
plan.srt_reads[1].value.Instruction()->Flags<SrtReadFlags>().clean == 0u &&
plan.srt_reads[2].value.Instruction()->Flags<SrtReadFlags>().clean == 1u,
"alias normalization lost read identity or retained discarded EXEC provenance");
Check(plan.srt_code == nullptr,
"captured strict reads compiled an unused x86 walker");
struct Reads { uint32_t strict = 0; uint32_t ordinary = 0; bool dirty = false; } reads;
const SrtRuntime runtime{
.read_memory = +[](void *data, uint64_t address, std::span<uint32_t> words) {
++static_cast<Reads *>(data)->ordinary;
if (address == 0x2000u) words[0] = 0x2222u;
else if (address == 0x3000u) words[0] = 0x8000u;
else if (address == 0x8000u) words[0] = 0xeeeeu;
else return false;
return true;
},
.userdata = &reads,
.read_specialization_memory = +[](void *data, uint64_t address,
std::span<uint32_t> words) {
Check(plan.descriptor_sources[0].dwords[0] == plan.srt_reads[0].value,
"alias normalization lost read identity");
struct Reads { uint32_t count = 0; bool dirty = false; } reads;
FakeGuestMemory fake(+[](void *data, uint64_t address, std::span<uint32_t> words) {
auto &reads = *static_cast<Reads *>(data);
++reads.strict;
if (reads.dirty) return false;
if (address == 0x3000u) words[0] = 0x1000u;
else if (address == 0x1000u) words[0] = 0x1111u;
++reads.count;
if (address == 0x1000u) words[0] = 0x1111u;
else if (address == 0x2000u) words[0] = 0x2222u;
else if (address == 0x3000u && !reads.dirty) words[0] = 0x1000u;
else return false;
return true;
}};
}, &reads);
std::vector<uint32_t> flat;
DescriptorValue descriptor;
{
SrtWalker clean(plan, CleanRuntime(runtime));
SrtWalker walker(plan, runtime, &clean);
Check(walker.RefreshFlatBuffer(flat) &&
flat == std::vector<uint32_t>{0x1111u, 0x2222u, 0x1000u} &&
walker.EvaluateDescriptor(0, descriptor) && descriptor.dwords[0] == 0x1111u &&
reads.strict == 2 && reads.ordinary == 1,
"normalized aliases changed nested strict reads or repeated a shared read");
}
Check(!SrtWalker(plan, runtime).RefreshFlatBuffer(flat),
"strict flat read accepted a missing clean evaluator");
auto no_reader = runtime;
no_reader.read_specialization_memory = nullptr;
{
SrtWalker clean(plan, CleanRuntime(no_reader));
Check(!SrtWalker(plan, no_reader, &clean).RefreshFlatBuffer(flat),
"strict flat read accepted a missing strict reader");
}
Check(SrtWalker(plan, runtime).EvaluateDescriptor(0, descriptor) &&
descriptor.dwords[0] == 0xeeeeu && reads.strict == 2 && reads.ordinary == 3,
"direct descriptor evaluation without a clean evaluator changed read domains");
reads.dirty = true;
{
SrtWalker clean(plan, CleanRuntime(runtime));
Check(!SrtWalker(plan, runtime, &clean).RefreshFlatBuffer(flat) &&
reads.strict == 3 && reads.ordinary == 3,
"dirty strict pointer fell back to an ordinary read");
}
Check(WalkSrt(plan, {}, flat, nullptr) && flat == std::vector<uint32_t>{0x1111u, 0x2222u, 0x1000u} &&
reads.count == 3,
"normalized aliases changed nested reads or repeated a shared read");
// An unreadable pointer reads as zero, and so does the null table it yields.
reads = {.dirty = true};
Check(WalkSrt(plan, {}, flat, nullptr) && flat == std::vector<uint32_t>{0u, 0x2222u, 0u} &&
reads.count == 3,
"unreadable pointer did not read as zero");
}
void TestUniformVectorDescriptorRead() {
@@ -477,59 +418,66 @@ void TestUniformVectorDescriptorRead() {
program.descriptor_sources.push_back(source);
program.info.buffers.push_back({.source = 0, .written = true});
Check(ValidateRuntimeValue(program, Value(&count)), "uniform DWORD count was rejected");
struct Reads { uint32_t value = 72; uint32_t strict = 0; uint32_t ordinary = 0; bool clean = true; } reads;
const SrtRuntime runtime{
.read_memory = [](void *data, uint64_t, std::span<uint32_t> words) {
++static_cast<Reads *>(data)->ordinary;
words[0] = 999;
return true;
},
.userdata = &reads,
.read_specialization_memory = [](void *data, uint64_t address, std::span<uint32_t> words) {
struct Reads { uint32_t value = 72; uint32_t count = 0; bool readable = true; } reads;
FakeGuestMemory fake([](void *data, uint64_t address, std::span<uint32_t> words) {
auto &reads = *static_cast<Reads *>(data);
++reads.strict;
if (!reads.clean || address != 0x1000u || words.size() != 1) return false;
++reads.count;
if (!reads.readable || address != 0x1000u || words.size() != 1) return false;
words[0] = reads.value;
return true;
}};
}, &reads);
const SrtRuntime runtime;
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
for (const bool written : {false, true}) {
program.info.buffers[0].written = written;
auto plan = ExtractResourcePlan(program);
Check(plan.control_flow.empty() && plan.requires_specialization_memory &&
plan.capture_specialization_reads,
Check(plan.control_flow.empty() && plan.capture_specialization_reads,
"vector descriptor read depended on incidental control-flow capture");
reads = {};
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
snapshot.buffers[0].dwords[2] == 145 && reads.strict == 1 && reads.ordinary == 0 &&
snapshot.buffers[0].dwords[2] == 145 && reads.count == 1 &&
snapshot.specialization_reads ==
std::vector<std::pair<uint64_t, uint64_t>>{{0x1000u, 4u}},
"vector descriptor input was not read and captured exactly once");
reads.clean = false;
reads.strict = 0;
Check(!MaterializeResources(plan, runtime, snapshot, specialization) &&
reads.strict == 1 && reads.ordinary == 0,
"dirty vector descriptor input fell back to an ordinary memory read");
// An unreadable input reads as zero, so the count selects 0 + 1.
reads = {.readable = false};
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
snapshot.buffers[0].dwords[2] == 1 && reads.count == 1,
"unreadable vector descriptor input did not read as zero");
}
// A read guarded by its EXEC mask depends on that mask, so it cannot reuse the unmasked read.
auto &guarded = block.AppendNewInst(ValueOpcode::LoadBufferU32,
{Value(&handle), Value(0u), Value(0u), Value(0u), Value(&active)});
guarded.SetFlags(MemoryFlags{.index = 0});
auto &guarded_lane = block.AppendNewInst(ValueOpcode::ReadFirstLane, {Value(&guarded), Value(&active)});
program.descriptor_sources.push_back({.dwords = {Value(&guarded_lane)}, .dword_count = 1});
{
auto plan = ExtractResourcePlan(program);
uint32_t value = 0;
reads = {};
Check(EvaluateSrtValue(plan, runtime, plan.descriptor_sources.back().dwords[0], value) &&
value == 72u && reads.count == 1,
"EXEC-guarded vector read lost its mask");
}
count.SetArg(4, Value(false));
auto &inactive = block.AppendNewInst(ValueOpcode::ReadFirstLane, {Value(&count), Value(false)});
program.descriptor_sources.push_back({.dwords = {Value(&inactive)}, .dword_count = 1});
reads.strict = 0;
reads.count = 0;
uint32_t result = 99;
{
const auto plan = ExtractResourcePlan(program);
Check(SrtWalker(plan, runtime).Evaluate(plan.descriptor_sources.back().dwords[0], result) &&
result == 0 && reads.strict == 0 && reads.ordinary == 0,
auto plan = ExtractResourcePlan(program);
Check(EvaluateSrtValue(plan, runtime, plan.descriptor_sources.back().dwords[0], result) &&
result == 0 && reads.count == 0,
"literal false EXEC read vector memory");
}
count.SetArg(4, Value(true));
handle.SetArg(3, Value(0x204u));
program.descriptor_sources.back().dwords[0] = Value(&count);
{
const auto plan = ExtractResourcePlan(program);
Check(SrtWalker(plan, runtime).Evaluate(plan.descriptor_sources.back().dwords[0], result) &&
result == 0 && reads.strict == 0 && reads.ordinary == 0,
auto plan = ExtractResourcePlan(program);
Check(EvaluateSrtValue(plan, runtime, plan.descriptor_sources.back().dwords[0], result) &&
result == 0 && reads.count == 0,
"invalid vector buffer format read memory");
}
handle.SetArg(3, Value(0x16204u));
@@ -547,7 +495,7 @@ void TestExactReciprocalDescriptorArithmetic() {
std::bit_cast<float>(253u << 23u)}) {
auto &reciprocal = block.AppendNewInst(ValueOpcode::FPRecipIFlag32, {Value::F32(divisor)});
uint32_t result = 0;
Check(SrtWalker(program, {}).Evaluate(Value(&reciprocal), result) &&
Check(EvaluateSrtValue(program, {}, Value(&reciprocal), result) &&
result == std::bit_cast<uint32_t>(1.f / divisor),
"power-of-two reciprocal was not exact");
}
@@ -556,7 +504,7 @@ void TestExactReciprocalDescriptorArithmetic() {
std::bit_cast<float>(254u << 23u)}) {
auto &reciprocal = block.AppendNewInst(ValueOpcode::FPRecipIFlag32, {Value::F32(divisor)});
uint32_t result = 0;
Check(!SrtWalker(program, {}).Evaluate(Value(&reciprocal), result),
Check(!EvaluateSrtValue(program, {}, Value(&reciprocal), result),
"unsupported reciprocal rounding or exceptional input was accepted");
}
}
@@ -593,7 +541,7 @@ void TestWrittenDescriptorPredicateReads(bool memory_condition) {
source.dword_count = 4;
program.descriptor_sources.push_back(source);
program.info.buffers.push_back({.source = 0, .written = true});
// Only memory-derived host decisions need clean, disjoint descriptor reads.
// Only memory-derived host decisions capture their descriptor reads.
auto &condition = block.AppendNewInst(ValueOpcode::IEqual32,
{memory_condition ? Value(&read) : Value(&offset),
Value(memory_condition ? 0x8000u : 4u)});
@@ -619,35 +567,25 @@ void TestWrittenDescriptorPredicateReads(bool memory_condition) {
auto plan = ExtractResourcePlan(program);
Check(plan.capture_specialization_reads == memory_condition && !plan.control_flow.empty(),
"conditional writable descriptor used the wrong memory dependency policy");
struct Reads { uint32_t ordinary = 0; uint32_t strict = 0; bool clean = false; } reads;
struct Reads { uint32_t count = 0; bool readable = false; } reads;
const std::array<uint32_t, 1> user_data{4u};
const SrtRuntime runtime{
.user_data = user_data,
.read_memory = +[](void *data, uint64_t, std::span<uint32_t> words) {
++static_cast<Reads *>(data)->ordinary;
words[0] = 0x8000u;
return true;
},
.userdata = &reads,
.read_specialization_memory = +[](void *data, uint64_t address, std::span<uint32_t> words) {
FakeGuestMemory fake(+[](void *data, uint64_t address, std::span<uint32_t> words) {
auto &reads = *static_cast<Reads *>(data);
++reads.strict;
if (!reads.clean || address != 0x1004u) return false;
++reads.count;
if (!reads.readable || address != 0x1004u) return false;
words[0] = 0x8000u;
return true;
}};
}, &reads);
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
for (const bool clean : {false, true}) {
reads = {.clean = clean};
// A failed predicate explores both edges; its dirty read also rejects the writable descriptor.
const bool expected = !memory_condition || clean;
Check(MaterializeResources(plan, runtime, snapshot, specialization) == expected &&
reads.ordinary == (memory_condition ? 0u : 1u) &&
reads.strict == (memory_condition ? 1u : 0u),
"writable descriptor changed reader policy or bypassed a dirty memory predicate");
if (!expected) continue;
Check(snapshot.buffers[0].dwords[0] == 0x8000u &&
for (const bool readable : {false, true}) {
reads = {.readable = readable};
// The predicate and the descriptor share one read. An unreadable one is zero, which
// makes a memory predicate skip the store block and leave its descriptor inactive.
Check(MaterializeResources(plan, {.user_data = user_data}, snapshot, specialization) &&
reads.count == 1,
"writable descriptor repeated or skipped its shared read");
Check(snapshot.buffers[0].dwords[0] == (readable ? 0x8000u : 0u) &&
snapshot.specialization_reads == (memory_condition
? std::vector<std::pair<uint64_t, uint64_t>>{{0x1004u, 4u}}
: std::vector<std::pair<uint64_t, uint64_t>>{}),
@@ -704,16 +642,13 @@ void TestSharedControlPredicates() {
"shared predicates expanded into trees or retained varying dependencies");
for (const uint32_t flag : {0u, 1u}) {
const std::array<uint32_t, 1> user_data{flag};
SrtWalker walker(plan, {.user_data = user_data});
uint32_t result = 99;
const bool conjunction = operation == ValueOpcode::LogicalAnd;
const bool known = conjunction ? flag == 0 : flag != 0;
Check(walker.Evaluate(plan.control_flow[0].condition, result) == known &&
Check(EvaluateSrtValue(plan, {.user_data = user_data}, plan.control_flow[0].condition, result) == known &&
(!known || result == uint32_t{conjunction}),
"shared predicate changed its uniform factor or cached runtime values");
std::vector<uint32_t> flat;
Check(walker.RefreshFlatBuffer(flat) &&
plan.visited_blocks == (known
Check(ReachedBlocks(plan, {.user_data = user_data}) == (known
? (conjunction ? std::vector<uint8_t>{1, 1, 0, 1}
: std::vector<uint8_t>{1, 0, 1, 0})
: std::vector<uint8_t>{1, 1, 1, 1}),
@@ -772,34 +707,27 @@ void TestNestedExecPredicatesReuseReads() {
{buffer, Value(0u), Value(0u), Value(0u), Value(true)})
.SetFlags(MemoryFlags{.index = 1});
auto plan = ExtractResourcePlan(program);
struct Reads { uint32_t value = 0; bool clean = true; uint32_t strict = 0, ordinary = 0; } reads;
const SrtRuntime runtime{
.read_memory = +[](void *data, uint64_t, std::span<uint32_t>) {
++static_cast<Reads *>(data)->ordinary;
return false;
},
.userdata = &reads,
.read_specialization_memory = +[](void *data, uint64_t address, std::span<uint32_t> words) {
struct Reads { uint32_t value = 0; bool readable = true; uint32_t count = 0; } reads;
FakeGuestMemory fake(+[](void *data, uint64_t address, std::span<uint32_t> words) {
auto &reads = *static_cast<Reads *>(data);
++reads.strict;
Check(reads.strict <= depth + 1,
"shared EXEC predicates repeated scalar reads beyond their distinct masks");
if (!reads.clean || address != 0x1000u || words.size() != 1) return false;
++reads.count;
if (!reads.readable || address != 0x1000u || words.size() != 1) return false;
words[0] = reads.value;
return true;
}};
}, &reads);
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
for (const auto state : {Reads{64u, true}, Reads{0u, true}, Reads{0u, false}, Reads{64u, true}}) {
for (const auto state : {Reads{64u, true}, Reads{0u, true}, Reads{64u, false}, Reads{64u, true}}) {
reads = state;
Check(MaterializeResources(plan, runtime, snapshot, specialization),
Check(MaterializeResources(plan, {}, snapshot, specialization),
"nested EXEC predicates rejected a valid resource snapshot");
const bool inactive = reads.clean && reads.value == 0;
// An unreadable count reads as zero.
const bool inactive = !reads.readable || reads.value == 0;
Check(snapshot.buffers[0].dwords[0] == (inactive ? 0u : 0x8000u),
"nested EXEC predicates lost an unknown branch or retained stale runtime facts");
Check(reads.strict > 0 && reads.ordinary == 0 &&
snapshot.specialization_reads.size() <= depth + 1,
"shared EXEC predicates repeated scalar reads beyond their distinct masks");
Check(reads.count == 1 && snapshot.specialization_reads ==
std::vector<std::pair<uint64_t, uint64_t>>{{0x1000u, 4u}},
"EXEC contexts repeated a mask-independent scalar read");
}
}
@@ -845,16 +773,8 @@ void TestFiniteImageRefreshReusesScalarReads() {
struct Reads {
std::array<uint32_t, 3> words{0x100u, 0x200u, 0x200u};
std::array<uint32_t, 3> counts{};
uint32_t ordinary = 0;
} reads;
const SrtRuntime runtime{
.read_memory = +[](void *data, uint64_t, std::span<uint32_t>) {
++static_cast<Reads *>(data)->ordinary;
return false;
},
.userdata = &reads,
.read_specialization_memory = +[](void *data, uint64_t address,
std::span<uint32_t> words) {
FakeGuestMemory fake(+[](void *data, uint64_t address, std::span<uint32_t> words) {
if (words.size() != 1 || address < 0x1000u || address >= 0x100cu ||
(address & 3u) != 0) return false;
auto &reads = *static_cast<Reads *>(data);
@@ -862,7 +782,8 @@ void TestFiniteImageRefreshReusesScalarReads() {
++reads.counts[index];
words[0] = reads.words[index];
return true;
}};
}, &reads);
const SrtRuntime runtime;
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
const auto capacities = [&] {
@@ -881,15 +802,15 @@ void TestFiniteImageRefreshReusesScalarReads() {
};
Check(MaterializeResources(plan, runtime, snapshot, specialization),
"finite image materialization failed");
Check(reads.ordinary == 0 && reads.counts == std::array<uint32_t, 3>{1, 1, 1} &&
Check(reads.counts == std::array<uint32_t, 3>{1, 1, 1} &&
snapshot.specialization_reads.size() == 3,
"finite image candidates repeated scalar reads or bypassed clean provenance");
"finite image candidates repeated or skipped scalar reads");
check_mapping({0, 1, 1, 1});
const auto warm_capacities = capacities();
reads.words = {0x300u, 0x300u, 0x400u};
Check(MaterializeResources(plan, runtime, snapshot, specialization),
"finite image refresh failed after descriptor changes");
Check(reads.ordinary == 0 && reads.counts == std::array<uint32_t, 3>{2, 2, 2} &&
Check(reads.counts == std::array<uint32_t, 3>{2, 2, 2} &&
snapshot.specialization_reads.size() == 3 &&
snapshot.images[0].dwords[0] == 0x300u && snapshot.images[1].dwords[0] == 0x400u,
"finite image refresh retained old scalar values or repeated reads");
@@ -992,36 +913,35 @@ void TestEmptyTraversalSkipsUnmappedDescriptor(bool independent_mask, bool count
emit(use, ValueOpcode::LoadBufferU32, {buffer, Value(0u), Value(0u), Value(0u), Value(true)})
.Instruction()->SetFlags(MemoryFlags{.index = buffer_index});
auto plan = ExtractResourcePlan(program);
struct Reads { uint32_t count = 0; bool clean = true; uint32_t descriptors = 0; } reads;
const SrtRuntime runtime{
.read_memory = +[](void *data, uint64_t address, std::span<uint32_t> words) {
struct Reads { uint32_t count = 0; bool readable = true; uint32_t descriptors = 0; } reads;
FakeGuestMemory fake(+[](void *data, uint64_t address, std::span<uint32_t> words) {
constexpr std::array<uint32_t, 4> descriptor{0x8000u, 1u << 16u, 16u,
(3u << 28u) | 0x4dfacu};
auto &reads = *static_cast<Reads *>(data);
if (address == 0x1000u && reads.readable) {
words[0] = reads.count;
return true;
}
if (address < 0x2000u || address >= 0x2010u || address % 4u || words.size() != 1)
return false;
++static_cast<Reads *>(data)->descriptors;
++reads.descriptors;
words[0] = descriptor[(address - 0x2000u) / 4u];
return true;
},
.userdata = &reads,
.read_specialization_memory = +[](void *data, uint64_t address, std::span<uint32_t> words) {
const auto &reads = *static_cast<Reads *>(data);
if (!reads.clean || address != 0x1000u || words.size() != 1) return false;
words[0] = reads.count;
return true;
}};
}, &reads);
const SrtRuntime runtime;
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
for (const auto state : {Reads{0u, true}, Reads{4u, true}, Reads{0u, false}, Reads{0u, true}}) {
for (const auto state : {Reads{0u, true}, Reads{4u, true}, Reads{4u, false}, Reads{0u, true}}) {
reads = state;
if (!counted_loop) {
uint32_t condition = 99;
Check(!SrtWalker(plan, runtime).Evaluate(plan.control_flow[3].condition, condition),
Check(!EvaluateSrtValue(plan, runtime, plan.control_flow[3].condition, condition),
"direct merge evaluation used missing or previous runtime reachability");
}
Check(MaterializeResources(plan, runtime, snapshot, specialization),
"partial traversal proof rejected a valid resource snapshot");
const bool inactive = !independent_mask && reads.clean && reads.count == 0u;
// An unreadable count reads as zero.
const bool inactive = !independent_mask && (!reads.readable || reads.count == 0u);
Check(snapshot.buffers[0].dwords[0] == (inactive ? 0u : 0x8000u),
"empty traversal kept its descriptor or an uncertain traversal lost it");
Check(specialization.buffers[0].zero_stride_oob == inactive,
@@ -1029,7 +949,7 @@ void TestEmptyTraversalSkipsUnmappedDescriptor(bool independent_mask, bool count
Check(reads.descriptors == (inactive ? 0u : 4u),
"inactive traversal read its descriptor or an uncertain traversal skipped it");
if (inactive) {
Check(!snapshot.specialization_reads.empty(), "empty traversal lost its clean scalar read");
Check(!snapshot.specialization_reads.empty(), "empty traversal lost its captured scalar read");
for (const auto &read : snapshot.specialization_reads)
Check(read == std::pair<uint64_t, uint64_t>{0x1000u, 4u},
"empty traversal proof captured an unrelated memory range");
@@ -1075,16 +995,89 @@ void TestCyclicControlPredicatesRemainUnknown() {
branch(left, a, right, exit);
branch(right, b, left, exit);
auto plan = ExtractResourcePlan(program);
SrtWalker walker(plan, {});
uint32_t value = 99;
Check(!walker.Evaluate(plan.control_flow[1].condition, value) &&
!walker.Evaluate(plan.control_flow[2].condition, value),
Check(!EvaluateSrtValue(plan, {}, plan.control_flow[1].condition, value) &&
!EvaluateSrtValue(plan, {}, plan.control_flow[2].condition, value),
"cyclic varying control acquired an unsupported uniform value");
std::vector<uint32_t> flat;
Check(walker.RefreshFlatBuffer(flat) && plan.visited_blocks == std::vector<uint8_t>({1, 1, 1, 1}),
Check(ReachedBlocks(plan, {}) == std::vector<uint8_t>({1, 1, 1, 1}),
"cyclic control lost a possible branch during reachability");
}
void TestBackEdgeReachesPhiPredecessor() {
using namespace Libs::Graphics::ShaderRecompiler::IR;
using Libs::Graphics::ShaderRecompiler::CFG::TerminatorKind;
Program program;
program.stage = Libs::Graphics::ShaderType::Compute;
program.user_data_count = 3;
program.srt_plan_complete = program.resource_tracking_complete = true;
for (uint32_t i = 0; i < 4; ++i) AddValueBlock(program);
auto &entry = *program.blocks[0];
auto &header = *program.blocks[1];
auto &body = *program.blocks[2];
auto &exit = *program.blocks[3];
const auto branch = [](Block &block, Block &target, Value condition = {}, Block *other = nullptr) {
block.terminator.kind = other ? TerminatorKind::ConditionalBranch : TerminatorKind::Branch;
block.terminator.true_block = &target;
block.terminator.false_block = other;
block.condition = condition;
block.AddBranch(&target);
if (other) block.AddBranch(other);
};
// header: p = Phi(entry: 0, body: 1); table[1] == p enters body, which returns to header. The
// first pass evaluates p and loads table[1] before body is reached; the walk must revisit both,
// capture the load once and reach exit, which loads table[0].
program.memory_info.push_back({.kind = ResourceKind::ScalarAddress});
const auto table = Value(&entry.AppendNewInst(ValueOpcode::GetAddressResource,
{Value(&entry.AppendNewInst(ValueOpcode::GetUserData, {Value(ScalarReg(0))})),
Value(&entry.AppendNewInst(ValueOpcode::GetUserData, {Value(ScalarReg(1))}))}));
const auto load = [&](Block &block, uint32_t offset) -> Inst & {
auto &read = block.AppendNewInst(ValueOpcode::LoadAddressU32, {table, Value(offset), Value(0u), Value(true)});
read.SetFlags(MemoryFlags{.index = 0});
return read;
};
auto &p = header.AppendNewInst(ValueOpcode::Phi);
p.SetFlags(Type::U32);
p.AddPhiOperand(&entry, Value(0u));
p.AddPhiOperand(&body, Value(1u));
branch(entry, header);
branch(header, body, Value(&header.AppendNewInst(ValueOpcode::IEqual32, {Value(&load(header, 4u)), Value(&p)})),
&exit);
branch(body, header);
exit.terminator.kind = TerminatorKind::Return;
program.srt_reads.push_back({Value(&load(exit, 0u)), 0});
exit.AppendNewInst(ValueOpcode::ReadConst,
{Value(&exit.AppendNewInst(ValueOpcode::GetSrtResource)), Value(0u)});
auto plan = ExtractResourcePlan(program);
Check(plan.control_flow.size() == 4 && plan.control_phi_predecessors.size() == 1,
"back-edge plan lost its control flow or Phi");
plan.capture_specialization_reads = true;
const std::array<uint32_t, 2> user_data{0x1000u, 0u};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
{
// The table is unmapped: its loads read zero instead of faulting the walk.
FakeGuestMemory unmapped;
Check(MaterializeResources(plan, {.user_data = user_data}, snapshot, specialization) &&
snapshot.flattened_srt == std::vector<uint32_t>{0u},
"faulting load in a reachable block did not read zero");
}
for (uint32_t selected : {0u, 1u}) {
FakeGuestMemory fake(+[](void *userdata, uint64_t address, std::span<uint32_t> words) {
words[0] = address == 0x1000u ? 0x5678u : *static_cast<uint32_t *>(userdata);
return address == 0x1000u || address == 0x1004u;
}, &selected);
Check(MaterializeResources(plan, {.user_data = user_data}, snapshot, specialization) &&
snapshot.flattened_srt == std::vector<uint32_t>{0x5678u},
"back edge to an evaluated Phi left its exit unreachable");
Check(snapshot.specialization_reads ==
std::vector<std::pair<uint64_t, uint64_t>>{{0x1004u, 4u}, {0x1000u, 4u}},
"Phi restart kept the loads of the abandoned pass");
Check(ReachedBlocks(plan, {.user_data = user_data}) ==
(selected == 0u ? std::vector<uint8_t>{1, 1, 1, 1} : std::vector<uint8_t>{1, 1, 0, 1}),
"Phi restart followed a rejected edge or missed a reachable one");
}
}
void TestEmptyScalarBufferSpecialization() {
using namespace Libs::Graphics::ShaderRecompiler::IR;
struct Fixture {
@@ -1144,8 +1137,9 @@ void TestEmptyScalarBufferSpecialization() {
auto plan = ExtractResourcePlan(fixture.program);
std::array<uint32_t, 4> descriptor{0x1000u, 8u << 16u, 0u, 3u << 28u};
uint32_t reads = 0u;
const SrtRuntime runtime{.user_data = descriptor, .read_memory = RejectSpecializationRead,
.userdata = &reads, .read_specialization_memory = RejectSpecializationRead};
const SrtRuntime runtime{.user_data = descriptor, .userdata = &reads,
.read_specialization_memory = RejectSpecializationRead};
FakeGuestMemory fake(RejectSpecializationRead, &reads);
ResourceSnapshot snapshot;
ResourceSpecialization specialization, empty, populated;
for (const uint32_t records : {0u, 1u, 0x20000000u, 0u}) {
@@ -1377,10 +1371,10 @@ void DbgExit(int) { std::abort(); }
} // namespace Common
int main() {
TestMappedSrtUsesDirectReaderByDefault();
TestWalkerLoadsBypassSpecializationReader();
TestNativeNestedSrtRefresh();
TestIntegerRuntimeValueFollowsSrtReads();
TestSrtAliasesRetainReadPolicy();
TestSrtAliasesShareReads();
TestUniformVectorDescriptorRead();
TestExactReciprocalDescriptorArithmetic();
TestUnbasedFlatCacheHitMaterializes();
@@ -1394,6 +1388,7 @@ int main() {
TestEmptyTraversalSkipsUnmappedDescriptor(independent_mask);
TestEmptyTraversalSkipsUnmappedDescriptor(false, true);
TestCyclicControlPredicatesRemainUnknown();
TestBackEdgeReachesPhiPredecessor();
TestEmptyScalarBufferSpecialization();
TestMixedSamplerVariantsShareRuntimeDescriptor();
TestSignedStorageSpecialization();
+152 -154
View File
@@ -5,7 +5,7 @@
#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 "SrtWalkerTestMemory.h"
#include <array>
#include <bit>
@@ -212,6 +212,12 @@ bool ReadLinearTestMemory(void *userdata, uint64_t address, std::span<uint32_t>
return true;
}
// Serves walker loads from the memory a test runtime currently points at.
bool ReadRuntimeMemory(void *userdata, uint64_t address, std::span<uint32_t> values) {
const auto &runtime = *static_cast<const SrtRuntime *>(userdata);
return runtime.read_specialization_memory(runtime.userdata, address, values);
}
std::unique_ptr<Fixture>
MakeIndirectImageFixture(bool malformed, uint32_t table_stride = 32, uint32_t table_offset = 0,
uint32_t table_shader_offset = 0, bool nested = false,
@@ -424,9 +430,8 @@ void TestInvariantIndirectImageMaterialization(bool nested) {
const auto handle = std::ranges::find_if(*wrap->block, [](const Inst &inst) {
return inst.GetOpcode() == ValueOpcode::GetImageResource;
});
SrtWalker walker(wrap->program, runtime);
uint32_t offset = 0;
Check(walker.Evaluate(handle->Arg(0), offset) && offset == expected,
Check(EvaluateSrtValue(wrap->program, runtime, handle->Arg(0), offset) && offset == expected,
"GPU descriptor offset projection lost U32 wrap or scalar immediate carry");
}
auto malformed = MakeIndirectImageFixture(true);
@@ -624,6 +629,7 @@ void TestBoundedImageViewEligibility() {
plan.descriptor_sources.push_back(source);
}
plan.descriptor_sources[image_source].indirect_descriptor->sources = std::move(selected);
CompileSrtWalker(plan);
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
"explicitly selected incompatible descriptor was silently normalized to null");
}
@@ -873,9 +879,8 @@ void TestWaterfallImageTable() {
std::array{UINT32_MAX, 0x5204u, UINT32_MAX}, std::array{0u, 0u, UINT32_MAX}}) {
data[4] = index;
data[3] = format;
SrtWalker walker(fixture->program, runtime);
uint32_t offset = 0;
Check(walker.Evaluate(handle->Arg(0), offset) && offset == expected,
Check(EvaluateSrtValue(fixture->program, runtime, handle->Arg(0), offset) && offset == expected,
"VMEM table projection lost record bounds, format disable or multiplication carry");
}
auto disabled = make(true, false);
@@ -1113,7 +1118,7 @@ void TestNestedPointerImageTable() {
memory.fail_address = 0x1800u;
*at(table) = 0u;
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
"unreadable null pointer row bypassed the strict pointee reader");
"unreadable null pointer row bypassed the specialization reader");
*at(table) = objects;
auto capped = make(4u);
@@ -1381,7 +1386,7 @@ void TestIndexedPointerImageTable() {
*at(table) = 0u;
*at(table + 4u) = 0u;
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
"explicit null pointer bypassed the strict child reader");
"explicit null pointer bypassed the specialization reader");
auto reversed = make(Case::ReverseSigned);
reversed->PlanAndTrack();
for (const auto variant : {Case::MissingParent, Case::WrongParent, Case::MissingChild,
@@ -1398,8 +1403,9 @@ void TestIndexedPointerImageTable() {
const auto counted_plan = ExtractResourcePlan(counted->program);
constexpr uint32_t count_address = 0x1800u;
std::array<uint32_t, 6> counted_data{table, 1u | (stride << 16u), 2u, 0x5204u, count_address, 1u};
const SrtRuntime counted_runtime{.user_data = counted_data, .read_memory = ReadLinearTestMemory, .userdata = &memory,
const SrtRuntime counted_runtime{.user_data = counted_data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
FakeGuestMemory fake(ReadLinearTestMemory, &memory);
*at(table) = objects;
*at(table + 4u) = 0xbeef0001u;
*at(count_address) = *at(count_address + 4u) = 2u;
@@ -1431,9 +1437,11 @@ void TestIndexedPointerImageTable() {
Check(!MaterializeResources(counted_plan, counted_runtime, snapshot, specialization) && memory.watched_reads == 0u,
"runtime parent count exceeding V# records was clamped or read candidates");
*at(count_address) = 2u;
// An unreadable parent count reads as zero: an empty domain without candidate reads.
memory.fail_address = high + count_address;
Check(!MaterializeResources(counted_plan, counted_runtime, snapshot, specialization) && memory.watched_reads == 0u,
"unreadable parent count was ignored before candidate reads");
Check(MaterializeResources(counted_plan, counted_runtime, snapshot, specialization) &&
snapshot.images.size() == 1u && memory.watched_reads == 0u,
"unreadable parent count did not read as an empty domain");
memory.fail_address = UINT64_MAX;
for (const auto variant : {Case::MissingCount, Case::DifferentCount, Case::UnprovenMask}) {
auto unproved = make(variant);
@@ -1780,9 +1788,9 @@ void TestGuardedDirectImageTable() {
};
fill_table(memory, table);
std::array<uint32_t, 2> user_data{0x1000u, 0u};
SrtRuntime runtime{.user_data = user_data, .read_memory = ReadLinearTestMemory,
.userdata = &memory,
SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
FakeGuestMemory fake(ReadRuntimeMemory, &runtime);
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
auto packed = make_plan(Guard::Plain, 0u, true);
@@ -2352,10 +2360,9 @@ void TestBoundedComputeImageLoop() {
(static_cast<uint32_t>(Libs::Graphics::Prospero::ImageType::kColor2D) << 28u);
}
std::array<uint32_t, 3> user_data{0x1800u, 0u, 2u};
SrtRuntime runtime{.user_data = user_data,
.read_memory = ReadLinearTestMemory,
.userdata = &memory,
SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
FakeGuestMemory fake(ReadLinearTestMemory, &memory);
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
for (const auto post : {PostTest::Signed, PostTest::Unsigned}) {
@@ -2668,8 +2675,8 @@ void TestDescendingImageRange() {
std::array<uint32_t, 4> user_data {0x1000u, 0u, 0u, UINT32_MAX};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
const SrtRuntime runtime {.user_data = user_data, .read_memory = ReadLinearTestMemory,
.userdata = &memory, .read_specialization_memory = ReadLinearTestMemory};
const SrtRuntime runtime {.user_data = user_data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
for (const auto count : {0u, 2u, 0u}) {
user_data[2] = count;
Check(MaterializeResources(plan, runtime, snapshot, specialization) && snapshot.images.size() == 2u,
@@ -3176,7 +3183,7 @@ void TestUniformizedMaterialImageKeys() {
make_plan(Variant::FirstLane);
make_plan(Variant::FirstLaneDirect);
plan = make_plan(Variant::FirstLaneAndNot);
Check(plan.requires_specialization_memory &&
Check(plan.capture_specialization_reads &&
plan.descriptor_sources[plan.info.images[0].source]
.indirect_descriptor->selector_mask.Resolve().TryInstruction() != nullptr,
"uniformized image mask did not survive extraction");
@@ -3207,10 +3214,8 @@ void TestUniformizedMaterialImageKeys() {
user_data[9] = 16u << 16u;
user_data[10] = 1u;
memory.fail_address = base + 0xc00u + 3u * 0x90u;
const SrtRuntime runtime{
.user_data = user_data, .read_memory = ReadLinearTestMemory,
.userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
const SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
@@ -3388,7 +3393,7 @@ void TestIndexedMaterialImageKeys() {
if (shared) {
owner_key.ResolveInstruction()->ReplaceUsesWith(Value(12994u));
uint32_t offset = 0u;
Check(SrtWalker(f.program, {}).Evaluate(shared_image.ResolveInstruction()->Arg(0), offset) &&
Check(EvaluateSrtValue(f.program, {}, shared_image.ResolveInstruction()->Arg(0), offset) &&
offset == 12994u * 32u,
"inherited indexed selector changed GPU byte offsets into raw material keys");
}
@@ -3811,10 +3816,9 @@ void TestUniformScalarBufferImage() {
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,
SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
FakeGuestMemory fake(ReadLinearTestMemory, &memory);
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
const auto materializes = [&](uint32_t address) {
@@ -3827,9 +3831,10 @@ void TestUniformScalarBufferImage() {
memory.words[0x44u / 4u] = 0u;
memory.words[0x1010u / 4u] = 0x30u;
Check(materializes(0x30u), "changed scalar table memory reused the previous image descriptor");
// An unreadable key reads as zero and selects the first record instead of the second.
memory.words[0x44u / 4u] = 1u;
memory.fail_address = 0x1044u;
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
"unavailable nested image table was accepted");
Check(materializes(0x30u), "unavailable nested image key did not read as zero");
}
void TestCompactScalarImageTable() {
@@ -4258,8 +4263,8 @@ void TestScalarBufferSelectorDomain() {
(static_cast<uint32_t>(Libs::Graphics::Prospero::ImageType::kColor2D) << 28u);
}
std::array<uint32_t, 6> user_data {0x1000u, 0u, 180u, 0u, 0x2000u, 0u};
const SrtRuntime runtime {.user_data = user_data, .read_memory = ReadLinearTestMemory,
.userdata = &memory, .read_specialization_memory = ReadLinearTestMemory};
const SrtRuntime runtime {.user_data = user_data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
memory.watched_address = 0x1000u + 144u;
@@ -4314,8 +4319,8 @@ void TestScalarBufferSelectorDomain() {
user_data[1] = 2u << 16u;
user_data[2] = UINT32_MAX;
Check(MaterializeResources(plan,
{.user_data = user_data, .read_memory = read_sparse, .userdata = &sparse,
.read_specialization_memory = read_sparse}, snapshot, specialization) &&
{.user_data = user_data, .userdata = &sparse, .read_specialization_memory = read_sparse},
snapshot, specialization) &&
sparse.offsets == std::vector<uint64_t>{144u, 0x80000090u} &&
snapshot.images.size() == 2u,
"large V# bounds extended the native U32 domain or invented an OOB zero key");
@@ -4644,7 +4649,8 @@ void TestCountedRowImageSelector() {
at(MaterialBase + 256u) = 0x200u;
at(MaterialBase + 260u) = static_cast<uint32_t>(Libs::Graphics::Prospero::BufferFormat::k8SScaled) << 20u;
at(MaterialBase + 268u) = at(MaterialBase + 196u);
std::array<uint32_t, 12> user_data{KeyBase, 4u << 16u, 64u, 0x14004u,
// The walker needs every user-data DWORD the plan reads, including the zero s12-s14.
std::array<uint32_t, 15> user_data{KeyBase, 4u << 16u, 64u, 0x14004u,
CountBase, 4u << 16u, 4u, 0x14004u, MaterialBase, 396u << 16u, 4u, 0x14004u};
const SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
@@ -5002,8 +5008,9 @@ void TestSharedAppendSelectorBounds() {
(static_cast<uint32_t>(Libs::Graphics::Prospero::ImageType::kColor2D) << 28u);
}
std::array<uint32_t, 9> user_data {0x1000u, 160u << 16u, 8u, 0u, 0x2000u, 0u, 0x1800u, 0u, 0u};
const SrtRuntime runtime {.user_data = user_data, .read_memory = ReadLinearTestMemory,
.userdata = &memory, .read_specialization_memory = ReadLinearTestMemory};
SrtRuntime runtime {.user_data = user_data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
FakeGuestMemory fake(ReadRuntimeMemory, &runtime);
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
memory.fail_address = memory.watched_address = 0x1090u;
@@ -5085,9 +5092,8 @@ void TestSharedAppendSelectorBounds() {
user_data[2] = 65536u;
user_data[4] = 0x50000000u;
user_data[6] = 0x40000000u;
Check(MaterializeResources(plan,
{.user_data = user_data, .read_memory = read_boundary, .userdata = &boundary,
.read_specialization_memory = read_boundary}, snapshot, specialization) &&
runtime = {.user_data = user_data, .userdata = &boundary, .read_specialization_memory = read_boundary};
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
boundary.keys_read == 65536u && snapshot.images.size() == 1u &&
snapshot.images[0].dwords[0] == 0x101u,
"shared append materialization rejected its exact nonwrapping probe limit");
@@ -5103,9 +5109,7 @@ void TestSharedAppendSelectorBounds() {
for (const auto& test : overlaps) {
boundary.counts = test.counts;
boundary.keys_read = 0u;
Check(MaterializeResources(overlap_plan,
{.user_data = user_data, .read_memory = read_boundary, .userdata = &boundary,
.read_specialization_memory = read_boundary}, snapshot, specialization) &&
Check(MaterializeResources(overlap_plan, runtime, snapshot, specialization) &&
boundary.keys_read == test.expected[layout] && snapshot.images.size() == 1u &&
snapshot.images[0].dwords[0] == (test.expected[layout] != 0u ? 0x101u : 0u),
"shared append bounds lost overlap, included a touching list or retained stale keys");
@@ -5119,7 +5123,6 @@ void TestComputeBufferFill() {
bool conditional = false;
bool shifted = false;
bool extra_store = false;
bool clean = false;
bool branch = false;
};
const auto Run = [](Options options) {
@@ -5188,17 +5191,13 @@ void TestComputeBufferFill() {
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 auto materialize = [&](SrtMemoryReader reader) {
FakeGuestMemory fake(reader, &memory);
return MaterializeResources(plan, {.user_data = userdata}, snapshot, specialization);
};
Check(materialize(Read), "fill fixture did not materialize");
const bool expected = !options.conditional && !options.shifted &&
!options.extra_store && !options.branch &&
(!options.scalar || options.clean);
!options.extra_store && !options.branch;
Check((snapshot.uniform_fill.words != 0) == expected,
"fill proof accepted an unsafe store or missed the real GTA3 clear");
if (expected) {
@@ -5207,21 +5206,19 @@ void TestComputeBufferFill() {
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");
if (options.scalar) {
Check(materialize(nullptr) && snapshot.uniform_fill.words == 1 &&
snapshot.uniform_fill.value == 0u,
"an unreadable fill 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});
Run({.conditional = true});
Run({.shifted = true});
Run({.extra_store = true});
Run({.branch = true});
}
void TestDenseBufferTracking() {
@@ -5332,12 +5329,12 @@ void TestRuntimeUnsignedMinDescriptor() {
SrtRuntime runtime{.user_data = user_data};
DescriptorValue value;
const auto source = fixture.program.info.buffers[0].source;
Check(SrtWalker(plan, runtime).EvaluateDescriptor(source, value) &&
Check(EvaluateSrtDescriptor(plan, runtime, source, value) &&
value.dwords[3] == 0x100u,
"runtime descriptor unsigned minimum did not clamp its first operand");
user_data[0] = 0x80u;
Check(
SrtWalker(plan, runtime).EvaluateDescriptor(source, value) &&
EvaluateSrtDescriptor(plan, runtime, source, value) &&
value.dwords[3] == 0x80u,
"runtime descriptor unsigned minimum did not preserve its first operand");
}
@@ -5488,12 +5485,12 @@ void TestSampleAdjustSamplerScratch() {
std::array<uint32_t, 4> user_data{4u, 1u, 2u, 0x80000abcu};
SrtRuntime runtime{.user_data = user_data};
DescriptorValue descriptor;
Check(SrtWalker(plan, runtime).EvaluateDescriptor(source, descriptor) &&
Check(EvaluateSrtDescriptor(plan, runtime, source, descriptor) &&
descriptor.dwords[3] == 0x80000abcu,
"SampleAdjust canonicalization lost sampler border fields");
const auto shift_source = fixture.program.info.samplers[
shift_sampler.Instruction()->Flags<uint32_t>()].source;
Check(SrtWalker(plan, runtime).EvaluateDescriptor(shift_source, descriptor) &&
Check(EvaluateSrtDescriptor(plan, runtime, shift_source, descriptor) &&
std::ranges::equal(std::span(descriptor.dwords).first(4),
std::array{0x36u, 0xfff000u, 0x02500000u, 0u}),
"SampleAdjust shift-only scratch was not reduced to its zero border payload");
@@ -5601,11 +5598,11 @@ void TestFmaskLoadSpecialization() {
"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) &&
Check(EvaluateSrtValue(fixture.program, {.user_data = user_data}, 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) &&
Check(EvaluateSrtValue(fixture.program, {.user_data = user_data}, result, value) &&
value == 0u,
"inactive FMASK load did not preserve the execution mask");
ShaderComputeInputInfo compute{};
@@ -5849,16 +5846,14 @@ void TestSrtFlatteningAndRuntimeMemoization() {
std::array<uint32_t, 2> user_data{0x1000u, 0u};
TestMemory memory;
memory.words[1] = 0xdeadbeefu;
SrtRuntime runtime{.user_data = user_data,
.read_memory = ReadTestMemory,
.userdata = &memory};
SrtRuntime runtime{.user_data = user_data};
FakeGuestMemory fake(ReadTestMemory, &memory);
auto plan = ExtractResourcePlan(fixture.program);
DescriptorValue descriptor;
std::vector<uint32_t> 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);
return WalkSrt(plan, runtime, flat, nullptr) && WalkedDescriptor(plan, request, descriptor);
};
Check(refresh(plan), "typed runtime source evaluation failed");
Check(descriptor.dwords[0] == 0xdeadbeefu &&
@@ -5873,10 +5868,11 @@ void TestSrtFlatteningAndRuntimeMemoization() {
memory.reads = 0;
memory.fail_after = 0;
Check(!refresh(plan), "unavailable scalar memory was accepted");
Check(refresh(plan) && descriptor.dwords[0] == 0u && flat == std::vector<uint32_t>{0u},
"unavailable scalar memory did not read as zero");
memory.fail_after = UINT32_MAX;
Check(refresh(plan) && descriptor.dwords[0] == 0x12345678u && memory.reads == 1,
"failed runtime evaluation left a value marked as visiting");
"a zero read left a stale value in the next walk");
auto moved = std::move(plan);
memory.reads = 0;
@@ -5924,18 +5920,17 @@ void TestScalarDataReadControlFlow() {
{address, Value(0u), Value(0u), Value(true)}, fixture.AddMemory(scalar, 4), use);
fixture.PlanAndTrack();
auto plan = ExtractResourcePlan(fixture.program);
Check(plan.srt_reads.size() == 1 && !fixture.program.info.uses_dma &&
bool(plan.srt_code) == (comparison == ValueOpcode::Void),
"scalar data lost flattening or conditional reads entered the unconditional JIT");
Check(plan.srt_reads.size() == 1 && !fixture.program.info.uses_dma && plan.srt_code,
"scalar data lost flattening or its walker");
uint32_t reads = 0;
std::array<uint32_t, 2> user_data {};
SrtRuntime runtime {.user_data = user_data,
.read_memory = +[](void* data, uint64_t address, std::span<uint32_t> words) {
SrtRuntime runtime {.user_data = user_data};
FakeGuestMemory fake(+[](void* data, uint64_t address, std::span<uint32_t> words) {
++*static_cast<uint32_t*>(data);
if (address == 0) return false;
words[0] = 42;
return true;
}, .userdata = &reads};
}, &reads);
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
for (const uint64_t pointer : {uint64_t{0}, uint64_t{0x1000}, uint64_t{1} << 32u}) {
@@ -5977,11 +5972,10 @@ void TestDynamicSrtReadRemainsExplicit() {
std::array<uint32_t, 3> user_data{0x1000u, 0u, 4u};
TestMemory memory;
memory.words[1] = 0xabcdef01u;
SrtRuntime runtime{.user_data = user_data,
.read_memory = ReadTestMemory,
.userdata = &memory};
SrtRuntime runtime{.user_data = user_data};
FakeGuestMemory fake(ReadTestMemory, &memory);
DescriptorValue value;
Check(SrtWalker(plan, runtime).EvaluateDescriptor(fixture.program.info.buffers[0].source, value) &&
Check(EvaluateSrtDescriptor(plan, runtime, fixture.program.info.buffers[0].source, value) &&
value.dwords[0] == 0xabcdef01u && memory.reads == 1,
"dynamic typed scalar descriptor source was not evaluated");
@@ -6091,8 +6085,9 @@ void TestBoundedFormattedBufferStore() {
memory.words[word + 3u] = static_cast<uint32_t>(Prospero::BufferFormat::k32UInt) << 12u | 4u;
}
std::array<uint32_t, 2> user_data{static_cast<uint32_t>(memory.base), 0u};
SrtRuntime runtime{.user_data = user_data, .read_memory = ReadLinearTestMemory,
.userdata = &memory, .read_specialization_memory = ReadLinearTestMemory};
SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
FakeGuestMemory fake(ReadLinearTestMemory, &memory);
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
const auto materialize = [&] { return MaterializeResources(plan, runtime, snapshot, specialization); };
@@ -6316,8 +6311,9 @@ void TestBoundedBufferRecords(uint32_t d32_components) {
}
std::array<uint32_t, 6> user_data{uint32_t(memory.base) + table_offset, 8u << 16u,
4u * 192u / 8u, 0x5204u, uint32_t(memory.base), 0u};
SrtRuntime runtime{.user_data = user_data, .read_memory = ReadLinearTestMemory,
.userdata = &memory, .read_specialization_memory = ReadLinearTestMemory};
SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
FakeGuestMemory fake(ReadLinearTestMemory, &memory);
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
const auto materialize = [&] { return MaterializeResources(plan, runtime, snapshot, specialization); };
@@ -6498,8 +6494,8 @@ void TestGuardedWaterfallBufferRecords() {
std::copy(poison.begin(), poison.end(), memory.words.begin() + (table_offset + 480u) / 4u);
std::array<uint32_t, 8> user_data{uint32_t(memory.base) + table_offset, 488u << 16u,
2u, 0x5204u, 0x9000u, 4u << 16u, 32u, 0x5204u};
SrtRuntime runtime{.user_data = user_data, .read_memory = ReadLinearTestMemory,
.userdata = &memory, .read_specialization_memory = ReadLinearTestMemory};
SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
for (const auto guard : {Guard::Exact, Guard::Reversed, Guard::Greater, Guard::GreaterEqual,
@@ -6617,8 +6613,8 @@ void TestGpuSelectedBufferRecords() {
// SMEM aligns the base and dynamic offset independently; its V# format is ignored.
std::array<uint32_t, 8> user_data{uint32_t(memory.base) + table_offset + 3u, 0u, 0u, 0u,
uint32_t(memory.fail_address), 12u << 16u, 129600u, 0x5204u};
SrtRuntime runtime{.user_data = user_data, .read_memory = ReadLinearTestMemory,
.userdata = &memory, .read_specialization_memory = ReadLinearTestMemory};
SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
const auto materialize = [&] { return MaterializeResources(plan, runtime, snapshot, specialization); };
@@ -6709,8 +6705,8 @@ void TestWorkgroupSelectedBufferRecords() {
std::array<uint32_t, 8> user_data{uint32_t(memory.base) + table, 192u << 16u, 2u, 0x5204u,
uint32_t(memory.base) + selector, 12u << 16u, MaxIndirectDescriptorProbes, 0x5204u};
memory.watched_address = memory.base;
SrtRuntime runtime{.user_data = user_data, .read_memory = ReadLinearTestMemory,
.userdata = &memory, .read_specialization_memory = ReadLinearTestMemory,
SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory,
.indirect_args_address = memory.base};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
@@ -6938,8 +6934,9 @@ void TestGpuSelectedFormattedRecordStore() {
memory.fail_address = memory.base + 0x2000u;
std::array<uint32_t, 8> user_data{uint32_t(memory.base) + table_offset, 592u << 16u, 6u, 0x5204u,
uint32_t(memory.fail_address), 4u << 16u, 64u, 0x5204u};
SrtRuntime runtime{.user_data = user_data, .read_memory = ReadLinearTestMemory,
.userdata = &memory, .read_specialization_memory = ReadLinearTestMemory};
SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
FakeGuestMemory fake(ReadRuntimeMemory, &runtime);
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
const auto materialize = [&] { return MaterializeResources(plan, runtime, snapshot, specialization); };
@@ -7127,9 +7124,8 @@ void TestGpuSelectedFormattedRecordStore() {
std::array<uint32_t, 8> prefix_data{uint32_t(prefix_memory.base) + table_offset - 48u, 0u, 0u, 0u,
uint32_t(prefix_memory.base) + selector_offset, 8u << 16u, 12u, 0x5204u};
std::array counts{2u, 1u, 1u};
SrtRuntime prefix_runtime{.user_data = prefix_data, .read_memory = read_prefix,
.userdata = &prefix_memory, .read_specialization_memory = read_prefix,
.workgroup_counts = counts};
runtime = {.user_data = prefix_data, .userdata = &prefix_memory,
.read_specialization_memory = read_prefix, .workgroup_counts = counts};
prefix_memory.watched_address = prefix_memory.base + selector_offset + 4u;
for (const auto prefix : {Prefix::Forward, Prefix::Reversed}) {
const auto selected = make(16u, 16u, false, true, prefix);
@@ -7143,7 +7139,7 @@ void TestGpuSelectedFormattedRecordStore() {
const auto refresh = [&] {
prefix_memory.watched_reads = 0u;
prefix_memory.read_attempts = 0u;
return MaterializeResources(selected_plan, prefix_runtime, snapshot, specialization);
return MaterializeResources(selected_plan, runtime, snapshot, specialization);
};
for (const uint32_t first : {0u, 2u, 4u, 0u}) {
prefix_memory.words[first_offset / 4u] = first;
@@ -7155,9 +7151,12 @@ void TestGpuSelectedFormattedRecordStore() {
Check(std::ranges::find(snapshot.specialization_reads, read) != snapshot.specialization_reads.end(),
"runtime first or selected prefix escaped specialization read capture");
}
// An unreadable runtime first reads as zero.
prefix_memory.words[first_offset / 4u] = 4u;
prefix_memory.fail_address = prefix_memory.base + first_offset;
Check(!refresh() && prefix_memory.read_attempts == 1u,
"unreadable runtime first reached the selector payload");
Check(refresh() && snapshot.buffers.size() == selected_plan.info.buffers.size() + 1u &&
snapshot.buffers[wide_root].dwords[0] == 0x100000u,
"unreadable runtime first did not read as zero");
prefix_memory.fail_address = UINT64_MAX;
prefix_memory.words[first_offset / 4u] = 6u;
Check(!refresh(), "an actually selected invalid numeric descriptor was silently omitted");
@@ -7257,8 +7256,8 @@ void TestFiniteBufferSelection() {
}
memory.fail_address = memory.base + 32u + 5u * 16u;
const std::array<uint32_t, 2> user_data{static_cast<uint32_t>(memory.base), 0u};
SrtRuntime runtime{.user_data = user_data, .read_memory = ReadLinearTestMemory,
.userdata = &memory, .read_specialization_memory = ReadLinearTestMemory};
SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
Check(MaterializeResources(plan, runtime, snapshot, specialization) && snapshot.buffers.size() == 5u,
@@ -7339,8 +7338,8 @@ void TestPackedScalarBufferSelection() {
// No descriptor exists after key 18; broad enumeration would read this address.
memory.fail_address = memory.base + 0x170u;
const std::array<uint32_t, 2> user_data{static_cast<uint32_t>(memory.base), 0u};
const SrtRuntime runtime{.user_data = user_data, .read_memory = ReadLinearTestMemory,
.userdata = &memory, .read_specialization_memory = ReadLinearTestMemory};
const SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
const auto materialize = [&] { return MaterializeResources(plan, runtime, snapshot, specialization); };
@@ -7497,38 +7496,27 @@ void TestConditionalSamplerPhi() {
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};
FakeGuestMemory fake(ReadLinearTestMemory, &memory);
// 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<uint32_t>(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),
Check(EvaluateSrtDescriptor(plan, {}, 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");
}
// An unreadable negative predicate reads as zero and selects the nonnegative edge.
memory.words[(0x1000 + 196) / 4] = UINT32_MAX;
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");
}
DescriptorValue selected;
Check(EvaluateSrtDescriptor(plan, {}, source, selected) &&
selected.dwords[0] == memory.words[reverse ? 480 / 4 : 448 / 4],
"conditional sampler did not read unavailable predicate memory as zero");
}
}
CheckFatal([&] { ConditionalSamplerPlan(diamond, false, false, true); },
@@ -7592,10 +7580,9 @@ void TestFiniteImagePhiCycle() {
"finite image key did not preserve the loop-carried selection");
std::array<uint32_t, 16> data;
for (uint32_t i = 0; i < data.size(); ++i) data[i] = 100u + i;
SrtWalker walker(plan, SrtRuntime{.user_data = data});
for (uint32_t candidate = 0; candidate < 2; ++candidate) {
DescriptorValue descriptor;
Check(walker.EvaluateDescriptor(finite->sources[candidate], descriptor),
Check(EvaluateSrtDescriptor(plan, {.user_data = data}, finite->sources[candidate], descriptor),
"finite image candidate is not host-readable");
for (uint32_t word = 0; word < 8; ++word)
Check(descriptor.dwords[word] == data[candidate * 8 + word],
@@ -7666,8 +7653,8 @@ void TestPackedDescriptorWaterfall() {
memory.words[word + 1u] = 0x00fff000u;
memory.words[word + 2u] = (row & 1u) ? 0x0a500000u : 0u;
}
const SrtRuntime runtime{.user_data = user_data, .read_memory = ReadLinearTestMemory,
.userdata = &memory, .read_specialization_memory = ReadLinearTestMemory};
const SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
@@ -7795,8 +7782,9 @@ void TestFiniteImageWaterfall() {
(uint32_t(Libs::Graphics::Prospero::ImageType::kColor2D) << 28u);
}
std::array<uint32_t, 4> user_data{uint32_t(memory.base) + 256u, 8u << 16u, 60u, 0x5204u};
SrtRuntime runtime{.user_data = user_data, .read_memory = ReadLinearTestMemory,
.userdata = &memory, .read_specialization_memory = ReadLinearTestMemory};
SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
FakeGuestMemory fake(ReadLinearTestMemory, &memory);
ResourceSnapshot snapshot;
ResourceSpecialization specialization, populated;
for (const uint32_t count : {0u, 60u, 0u, 60u}) {
@@ -8048,7 +8036,7 @@ void TestLoopCycleEnteredThroughRuntimeValue() {
const std::array<uint32_t, 1> user_data{0x4000u};
DescriptorValue descriptor;
Check(fixture.program.info.buffers.size() == 1u &&
SrtWalker(plan, {.user_data = user_data}).EvaluateDescriptor(
EvaluateSrtDescriptor(plan, {.user_data = user_data},
fixture.program.info.buffers[0].source, descriptor) &&
descriptor.dwords[0] == user_data[0],
"runtime-rooted invariant loop lost its buffer source");
@@ -8079,7 +8067,7 @@ void TestInvariantLoopPhi() {
std::array<uint32_t, 1> user_data{0x12345678u};
SrtRuntime runtime{.user_data = user_data};
DescriptorValue descriptor;
Check(SrtWalker(plan, runtime).EvaluateDescriptor(fixture.program.info.buffers[0].source, descriptor) &&
Check(EvaluateSrtDescriptor(plan, runtime, fixture.program.info.buffers[0].source, descriptor) &&
descriptor.dwords[0] == user_data[0],
"loop-invariant descriptor phi was not evaluated through typed SSA");
}
@@ -8196,8 +8184,7 @@ void TestBoundedRelativeRegisterWrites() {
const std::array<uint32_t, 3> user_data{0x1000u, 0u, 72u};
SrtRuntime runtime{.user_data = user_data};
DescriptorValue descriptor;
Check(SrtWalker(plan, runtime).EvaluateDescriptor(
fixture.program.info.buffers[0].source, descriptor) &&
Check(EvaluateSrtDescriptor(plan, runtime, fixture.program.info.buffers[0].source, descriptor) &&
descriptor.dwords[2] == 72u,
"impossible relative writes left a false descriptor dependency");
}
@@ -8412,6 +8399,7 @@ void TestConditionalBufferMaterialization() {
TestMemory memory;
SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
.read_specialization_memory = ReadTestMemory};
FakeGuestMemory fake(ReadTestMemory, &memory);
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
@@ -8422,12 +8410,11 @@ void TestConditionalBufferMaterialization() {
Check(snapshot.user_data == std::vector<uint32_t>(user_data.begin(), user_data.end()),
"resource reachability changed native shader user data");
// The walker needs every user-data DWORD it may read, even for an untaken branch.
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");
"truncated user data was accepted");
memory.words[0] = 1;
runtime.user_data = user_data;
const auto CheckActive = [&] {
@@ -8438,9 +8425,13 @@ void TestConditionalBufferMaterialization() {
"potentially executed buffer descriptor was discarded");
};
CheckActive();
memory.words[0] = 0;
// An unreadable control word reads as zero and skips the branch.
memory.fail_after = memory.reads;
CheckActive();
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
snapshot.buffers.size() == 2 && snapshot.buffers[1].dwords == std::array<uint32_t, 8>{},
"unreadable scalar control word did not read as zero");
// Walker loads do not depend on the specialization reader.
memory.fail_after = UINT32_MAX;
runtime.read_specialization_memory = nullptr;
CheckActive();
}
@@ -8515,8 +8506,9 @@ void TestGuardedScalarDescriptorReads() {
return ReadLinearTestMemory(&memory.data, address, words);
};
const std::array<uint32_t, 5> user_data{0x1000u, 0u, 64u, 0u, 0x1040u};
const SrtRuntime runtime{.user_data = user_data, .read_memory = Read,
.userdata = &memory, .read_specialization_memory = Read};
const SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
.read_specialization_memory = Read};
FakeGuestMemory fake(Read, &memory);
memory.data.words[0xa8 / 4] = 0x2000u;
memory.data.words[0xb0 / 4] = 4u;
ResourceSnapshot snapshot;
@@ -8526,8 +8518,9 @@ void TestGuardedScalarDescriptorReads() {
std::ranges::all_of(snapshot.flattened_srt, [](auto word) { return word == 0; }),
"disabled feature speculatively dereferenced its null BVH table");
memory.data.words[7] = 1;
Check(!MaterializeResources(plan, runtime, snapshot, specialization) && memory.null_reads == 1,
"active feature accepted an unreadable BVH table");
Check(MaterializeResources(plan, runtime, snapshot, specialization) && memory.null_reads == 1 &&
std::ranges::all_of(snapshot.flattened_srt, [](auto word) { return word == 0; }),
"active feature did not read its unreadable null BVH table as zero");
}
memory.data.words[0x40 / 4] = 0x1080u;
memory.data.watched_address = 0x10a8u;
@@ -8557,19 +8550,24 @@ void TestConservativeBufferReachability() {
TestMemory memory;
const SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
.read_specialization_memory = ReadTestMemory};
FakeGuestMemory fake(ReadTestMemory, &memory);
for (const auto use : {ConditionalBufferUse::Shared, ConditionalBufferUse::Loop,
ConditionalBufferUse::Writable}) {
auto plan = ConditionalBufferPlan(use);
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
std::array<uint32_t, 4> expected{};
if (use != ConditionalBufferUse::Writable)
std::copy_n(user_data.begin() + 4, expected.size(), expected.begin());
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
snapshot.buffers.size() == 2 &&
std::equal(expected.begin(), expected.end(),
snapshot.buffers[1].dwords.begin()),
"shared or loop-dependent resource was pruned, or inactive scalar branch retained");
// With a zero control word, the loop body's only other predecessor is itself.
for (const uint32_t flag : {0u, 1u}) {
memory.words[0] = flag;
std::array<uint32_t, 4> expected{};
if (use == ConditionalBufferUse::Shared || flag != 0u)
std::copy_n(user_data.begin() + 4, expected.size(), expected.begin());
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
snapshot.buffers.size() == 2 &&
std::equal(expected.begin(), expected.end(),
snapshot.buffers[1].dwords.begin()),
"shared or entered loop resource was pruned, or an untaken branch or loop retained");
}
}
}
+49 -38
View File
@@ -4,7 +4,7 @@
#include "graphics/shader/recompiler/ir/passes/ReadLaneElimination.h"
#include "graphics/shader/recompiler/ir/passes/ResourceTracking.h"
#include "graphics/shader/recompiler/ir/passes/ResourceMaterialization.h"
#include "graphics/shader/recompiler/ir/passes/SrtWalker.h"
#include "SrtWalkerTestMemory.h"
#include <cstdlib>
#include <iostream>
@@ -95,6 +95,16 @@ bool ReadMemory(void *userdata, uint64_t address, std::span<uint32_t> values) {
return true;
}
// Evaluates the DWORDs of a descriptor source that no resource consumes.
bool EvaluateSource(ResourcePlan &plan, const SrtRuntime &runtime, uint32_t source,
DescriptorValue &result) {
const auto &words = plan.descriptor_sources[source];
result = {.dword_count = words.dword_count};
for (uint32_t word = 0; word < words.dword_count; ++word)
if (!EvaluateSrtValue(plan, runtime, words.dwords[word], result.dwords[word])) return false;
return true;
}
Value Address(Fixture &fixture, Value low, Value high, uint32_t block = 0) {
return fixture.Emit(ValueOpcode::GetAddressResource, {low, high}, 0, block);
}
@@ -132,9 +142,9 @@ void TestImmediateFlatteningAndGvn() {
"flattened raw read was not kept as a planning-only root");
TestMemory memory_image{{{0x1020u, 0xfeedbeefu}}};
SrtRuntime runtime{.read_memory = ReadMemory, .userdata = &memory_image};
FakeGuestMemory fake(ReadMemory, &memory_image);
std::vector<uint32_t> flat;
Check(SrtWalker(ExtractResourcePlan(fixture.program), runtime).RefreshFlatBuffer(flat), "flattened SRT walk failed");
Check(WalkSrt(ExtractResourcePlan(fixture.program), {}, flat, nullptr), "flattened SRT walk failed");
Check(flat == std::vector<uint32_t>{0xfeedbeefu} && memory_image.reads == 1,
"flattened SRT did not evaluate its canonical read once");
}
@@ -148,9 +158,9 @@ void TestRawScalarComponentAlignment() {
fixture.Plan();
TestMemory memory_image{{{0x1000u, 0x12345678u}}};
SrtRuntime runtime{.read_memory = ReadMemory, .userdata = &memory_image};
FakeGuestMemory fake(ReadMemory, &memory_image);
std::vector<uint32_t> flat;
Check(SrtWalker(ExtractResourcePlan(fixture.program), runtime).RefreshFlatBuffer(flat), "raw scalar SRT walk failed");
Check(WalkSrt(ExtractResourcePlan(fixture.program), {}, flat, nullptr), "raw scalar SRT walk failed");
Check(
flat == std::vector<uint32_t>{0x12345678u} && memory_image.reads == 1,
"raw scalar base, immediate, and offset were not aligned independently");
@@ -203,9 +213,9 @@ void TestNestedSrtWalk() {
fixture.Plan();
TestMemory memory_image{{{0x1000u, 0x2000u}, {0x2000u, 0xabcdef01u}}};
SrtRuntime runtime{.read_memory = ReadMemory, .userdata = &memory_image};
FakeGuestMemory fake(ReadMemory, &memory_image);
std::vector<uint32_t> flat;
Check(SrtWalker(ExtractResourcePlan(fixture.program), runtime).RefreshFlatBuffer(flat), "nested SRT walk failed");
Check(WalkSrt(ExtractResourcePlan(fixture.program), {}, flat, nullptr), "nested SRT walk failed");
Check(flat == std::vector<uint32_t>({0x2000u, 0xabcdef01u}),
"nested typed SRT reads were not evaluated in dependency order");
}
@@ -227,8 +237,9 @@ void TestShaderBaseAndUserData() {
const std::array user_data{0x20u};
SrtRuntime runtime{.user_data = user_data,
.shader_base = 0x12345678abcdef00ull};
auto plan = ExtractResourcePlan(fixture.program);
DescriptorValue result;
Check(SrtWalker(ExtractResourcePlan(fixture.program), runtime).EvaluateDescriptor(0, result),
Check(EvaluateSource(plan, runtime, 0, result),
"shader-relative descriptor evaluation failed");
Check(result.dword_count == 3 && result.dwords[0] == 0xabcdef00u &&
result.dwords[1] == 0x12345678u && result.dwords[2] == 0x24u,
@@ -249,8 +260,9 @@ void TestCarryAndBitFields() {
fixture.program.descriptor_sources.push_back(
{.dwords = {low, high, inserted, sign}, .dword_count = 4});
auto plan = ExtractResourcePlan(fixture.program);
DescriptorValue result;
Check(SrtWalker(ExtractResourcePlan(fixture.program), {}).EvaluateDescriptor(0, result),
Check(EvaluateSource(plan, {}, 0, result),
"carry and bit-field descriptor evaluation failed");
Check(result.dwords[0] == 1u && result.dwords[1] == 1u &&
result.dwords[2] == 0x89abcdefu && result.dwords[3] == 0xffffffffu,
@@ -273,12 +285,12 @@ void TestInvariantAndDivergentPhi() {
fixture.program.descriptor_sources.push_back(
{.dwords = {Value(&divergent)}, .dword_count = 1});
const auto plan = ExtractResourcePlan(fixture.program);
auto plan = ExtractResourcePlan(fixture.program);
DescriptorValue result;
Check(SrtWalker(plan, {}).EvaluateDescriptor(0, result) &&
Check(EvaluateSource(plan, {}, 0, result) &&
result.dwords[0] == 7u,
"loop-invariant typed phi was rejected");
Check(!SrtWalker(plan, {}).EvaluateDescriptor(1, result),
Check(!EvaluateSource(plan, {}, 1, result),
"divergent phi was accepted");
}
@@ -294,7 +306,7 @@ void TestOperandMutationLifecycle() {
source->ReplaceUsesWith(Value(0xaau));
uint32_t result = 0;
Check(source->UseCount() == 0 &&
SrtWalker(fixture.program, {}).Evaluate(inserted, result) && result == 0x3au,
EvaluateSrtValue(fixture.program, {}, inserted, result) && result == 0x3au,
"inline operand replacement changed the evaluated value");
for (const auto opcode : {ValueOpcode::GetImageResource, ValueOpcode::MakeImageAddress}) {
@@ -333,7 +345,7 @@ void TestOperandMutationLifecycle() {
const auto sum = fixture.Emit(ValueOpcode::IAdd32, {Value(&phi), Value(5u)}, 0, 5);
phi.ReplaceUsesWith(Value(6u));
Check(source->UseCount() == 0 && phi.NumPhiBlocks() == 0 && phi.NumArgs() == 1 &&
SrtWalker(fixture.program, {}).Evaluate(sum, result) && result == 11,
EvaluateSrtValue(fixture.program, {}, sum, result) && result == 11,
"Phi replacement retained incoming uses or changed the consumer value");
}
@@ -374,8 +386,9 @@ void TestRuntime64BitDescriptorOps() {
fixture.program.descriptor_sources.push_back(
{.dwords = {low, high}, .dword_count = 2});
auto plan = ExtractResourcePlan(fixture.program);
DescriptorValue result;
Check(SrtWalker(ExtractResourcePlan(fixture.program), {}).EvaluateDescriptor(0, result) &&
Check(EvaluateSource(plan, {}, 0, result) &&
result.dwords[0] == 0xabcdu && result.dwords[1] == 0x1235u,
"64-bit typed descriptor arithmetic evaluation is incorrect");
}
@@ -448,13 +461,13 @@ void TestUniformFirstLaneSamplerLod() {
std::array<uint32_t, 7> user_data{};
user_data[6] = 3u;
SrtRuntime runtime{.user_data = user_data};
const auto plan = ExtractResourcePlan(fixture.program);
auto plan = ExtractResourcePlan(fixture.program);
DescriptorValue result;
Check(SrtWalker(plan, runtime).EvaluateDescriptor(0, result) &&
Check(EvaluateSource(plan, runtime, 0, result) &&
result.dwords[0] == 0x00300300u,
"uniform sampler LOD evaluated incorrectly");
user_data[6] = 20u;
Check(SrtWalker(plan, runtime).EvaluateDescriptor(0, result) &&
Check(EvaluateSource(plan, runtime, 0, result) &&
result.dwords[0] == 0x00ffffffu,
"uniform sampler LOD clamp evaluated incorrectly");
}
@@ -489,13 +502,12 @@ void TestFloatComparisonDescriptorInputs() {
fixture.program.descriptor_sources.push_back(
{.dwords = {low, high}, .dword_count = 2});
const auto plan = ExtractResourcePlan(fixture.program);
auto plan = ExtractResourcePlan(fixture.program);
for (size_t index = 0; index < inputs.size(); index++) {
const auto &input = inputs[index];
const std::array user_data{input.bits};
DescriptorValue result;
Check(SrtWalker(plan, {.user_data = user_data})
.EvaluateDescriptor(0, result) &&
Check(EvaluateSource(plan, {.user_data = user_data}, 0, result) &&
result.dwords[0] == (flush && index < 4 ? 1 : input.less_equal) &&
result.dwords[1] == (flush && index < 4 ? 1 : input.greater_equal),
"descriptor comparison disagrees with native FP32 input mode");
@@ -551,7 +563,6 @@ void TestConstantBufferBounds() {
uint32_t immediate;
uint32_t stride;
uint32_t records;
bool valid;
uint32_t expected;
uint32_t reads;
};
@@ -565,17 +576,17 @@ void TestConstantBufferBounds() {
++probe.attempts;
return ReadMemory(&probe.memory, address, values);
};
// PS5 scalar-buffer loads return zero per out-of-range DWORD; an unreadable
// in-range address remains a failed memory read.
for (const auto &test : {Case{12u, 0u, 0u, 16u, true, 0xa5a5a5a5u, 1u},
Case{3u, 1u, 0u, 16u, true, 0x12345678u, 1u},
Case{0xfffffffcu, 4u, 0u, 16u, true, 0u, 0u},
Case{16u, 0u, 0u, 16u, true, 0u, 0u},
Case{12u, 0u, 0u, 14u, true, 0u, 0u},
Case{0u, 0u, 0u, 0u, true, 0u, 0u},
Case{8u, 0u, 0u, 16u, false, 0u, 1u},
Case{12u, 0u, 48u, 32768u, true, 0xa5a5a5a5u, 1u},
Case{0x6339c0u, 16u, 48u, 32768u, true, 0u, 0u}}) {
// PS5 scalar-buffer loads return zero per out-of-range DWORD without an access;
// an unreadable in-range address is accessed and reads as zero too.
for (const auto &test : {Case{12u, 0u, 0u, 16u, 0xa5a5a5a5u, 1u},
Case{3u, 1u, 0u, 16u, 0x12345678u, 1u},
Case{0xfffffffcu, 4u, 0u, 16u, 0u, 0u},
Case{16u, 0u, 0u, 16u, 0u, 0u},
Case{12u, 0u, 0u, 14u, 0u, 0u},
Case{0u, 0u, 0u, 0u, 0u, 0u},
Case{8u, 0u, 0u, 16u, 0u, 1u},
Case{12u, 0u, 48u, 32768u, 0xa5a5a5a5u, 1u},
Case{0x6339c0u, 16u, 48u, 32768u, 0u, 0u}}) {
Fixture fixture;
const auto memory = fixture.AddMemory(ResourceKind::ScalarBuffer, test.immediate);
const auto buffer =
@@ -588,12 +599,11 @@ void TestConstantBufferBounds() {
fixture.Plan();
MemoryProbe memory_image;
SrtRuntime runtime{.read_memory = read_memory, .userdata = &memory_image};
FakeGuestMemory fake(read_memory, &memory_image);
std::vector<uint32_t> flat;
Check(SrtWalker(ExtractResourcePlan(fixture.program), runtime).RefreshFlatBuffer(flat) == test.valid,
Check(WalkSrt(ExtractResourcePlan(fixture.program), {}, flat, nullptr),
"constant-buffer walk misaligned or wrapped its offset components");
Check((!test.valid || flat == std::vector<uint32_t>{test.expected}) &&
memory_image.attempts == test.reads,
Check(flat == std::vector<uint32_t>{test.expected} && memory_image.attempts == test.reads,
"constant-buffer walk read the wrong word or accessed an out-of-bounds address");
}
}
@@ -831,8 +841,9 @@ void TestUndefinedRuntimeValueFails() {
fixture.program.descriptor_sources.push_back(
{.dwords = {undef}, .dword_count = 1});
auto plan = ExtractResourcePlan(fixture.program);
DescriptorValue result;
Check(!SrtWalker(ExtractResourcePlan(fixture.program), {}).EvaluateDescriptor(0, result),
Check(!EvaluateSource(plan, {}, 0, result),
"undefined typed descriptor source was accepted");
}
+33 -50
View File
@@ -60,6 +60,7 @@
#include "libs/errno.h"
#include "loader/symbolDatabase.h"
#include "spirv-tools/libspirv.hpp"
#include "SrtWalkerTestMemory.h"
#if __has_include("graphics/host_gpu/renderer/renderTargetBarriers.h")
#error "legacy render-target barrier API must remain deleted"
@@ -1724,15 +1725,17 @@ CompiledShader CompileCase(const TestCase &test, u32 host_subgroup_size = 64) {
const ShaderRecompiler::IR::SrtRuntime runtime{
.user_data = options.user_data,
.shader_base = reinterpret_cast<uint64_t>(test.code.data()),
.read_memory = ReadTestMemory,
.userdata = const_cast<std::vector<u32> *>(&test.initial),
.read_specialization_memory = ReadTestMemory,
.workgroup_counts = workgroup_counts,
};
Require(test.name, "resource materialization",
ShaderRecompiler::IR::MaterializeResources(
resource_plan, runtime, resources, specialization),
"translated resources could not be materialized");
{
FakeGuestMemory fake(ReadTestMemory, runtime.userdata);
Require(test.name, "resource materialization",
ShaderRecompiler::IR::MaterializeResources(
resource_plan, runtime, resources, specialization),
"translated resources could not be materialized");
}
auto result = ShaderRecompiler::CompileProgram(
std::move(translated), options, specialization);
for (const auto &[text, expected] : test.decoded_counts) {
@@ -11235,7 +11238,7 @@ public:
};
const ShaderRecompiler::IR::SrtRuntime runtime{
.user_data = user_data, .shader_base = reinterpret_cast<uint64_t>(caller.data()),
.read_memory = read, .read_specialization_memory = read};
.read_specialization_memory = read};
auto source = ShaderRecompiler::PrepareShaderSource(caller, options);
auto translated = ShaderRecompiler::TranslateProgram(
ShaderRecompiler::RefreshShaderSource(source, runtime), options);
@@ -17158,12 +17161,7 @@ public:
stencil_compute.stage.resources = &stencil_snapshot;
Require(name, "stencil fill materialization",
ShaderRecompiler::IR::MaterializeResources(stencil_plan,
{.user_data = stencil_userdata, .userdata = &stencil_byte,
.read_specialization_memory = +[](void *data, uint64_t address, std::span<uint32_t> words) {
if (address != reinterpret_cast<uint64_t>(data) || words.size() != 1u) return false;
words[0] = *static_cast<uint32_t *>(data);
return true;
}}, stencil_snapshot, stencil_specialization),
{.user_data = stencil_userdata}, stencil_snapshot, stencil_specialization),
"captured stencil shader could not resolve its clear byte");
ShaderRecompiler::IR::ApplyResourceSpecialization(stencil_translated.program,
stencil_specialization);
@@ -39523,25 +39521,25 @@ void CheckRuntimeBufferRecords(VulkanHarness &vulkan) {
options.input_info.compute = &test.compute_info;
auto translated = ShaderRecompiler::TranslateProgram(test.code, options);
auto plan = ExtractResourcePlan(translated.program);
struct CountMemory { u32 value = 72; u32 reads = 0; bool clean = true; } memory;
SrtRuntime runtime{.user_data = test.user_data, .userdata = &memory};
struct CountMemory { u32 value = 72; u32 reads = 0; bool readable = true; } memory;
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
Require(test.name, "strict reader required",
!MaterializeResources(plan, runtime, snapshot, specialization),
"vector descriptor input bypassed the clean-memory reader");
runtime.read_specialization_memory = [](void *data, uint64_t address, std::span<u32> words) {
auto &memory = *static_cast<CountMemory *>(data);
++memory.reads;
if (!memory.clean || address != count_address || words.size() != 1) return false;
words[0] = memory.value;
return true;
const auto materialize = [&] {
FakeGuestMemory fake(+[](void *data, uint64_t address, std::span<u32> words) {
auto &memory = *static_cast<CountMemory *>(data);
++memory.reads;
if (!memory.readable || address != count_address) return false;
words[0] = memory.value;
return true;
}, &memory);
return MaterializeResources(plan, {.user_data = test.user_data}, snapshot, specialization);
};
memory.clean = false;
Require(test.name, "dirty input rejected",
!MaterializeResources(plan, runtime, snapshot, specialization),
"GPU-dirty count was accepted for host descriptor evaluation");
memory.clean = true;
// An unreadable count reads as zero, which leaves no records.
memory.readable = false;
Require(test.name, "unreadable input",
materialize() && memory.reads == 1 && snapshot.buffers[2].dwords[2] == 0,
"unreadable count did not read as zero");
memory.readable = true;
ResourceSpecialization initial_specialization;
// Reuse the same resource plan and compiled shader as the count and coverage change.
for (const u32 count : {0u, 72u, 512u, 8192u, 262144u}) {
@@ -39550,7 +39548,7 @@ void CheckRuntimeBufferRecords(VulkanHarness &vulkan) {
const u32 chunk = count < 512 ? 64 : count < 8192 ? 128 : count < 262144 ? 256 : 512;
const u32 records = (count + chunk - 1) / chunk;
Require(test.name, "cached descriptor refresh",
MaterializeResources(plan, runtime, snapshot, specialization) && memory.reads == 1 &&
materialize() && memory.reads == 1 &&
snapshot.buffers[2].dwords[2] == records &&
snapshot.specialization_reads ==
std::vector<std::pair<uint64_t, uint64_t>>{{count_address, 4}},
@@ -44598,7 +44596,6 @@ void CheckPs5GameExampleImageClearRuntimeShape() {
compute.thread_ids_num = 1;
compute.workgroup_register = 8;
u32 scalar_clear = 0x40404040u;
bool clean_scalar = false;
const auto Compile = [&](const std::vector<u32> &code) {
ShaderRecompiler::CompileOptions options;
@@ -44616,13 +44613,6 @@ void CheckPs5GameExampleImageClearRuntimeShape() {
const ShaderRecompiler::IR::SrtRuntime runtime{
.user_data = options.user_data,
.shader_base = reinterpret_cast<uint64_t>(code.data()),
.userdata = &scalar_clear,
.read_specialization_memory = clean_scalar
? +[](void *data, uint64_t address, std::span<uint32_t> words) {
if (address != reinterpret_cast<uint64_t>(data) || words.size() != 1u) return false;
words[0] = *static_cast<uint32_t *>(data);
return true;
} : nullptr,
};
Require("Ps5GameExampleImageClear", "resource materialization",
ShaderRecompiler::IR::MaterializeResources(
@@ -44780,7 +44770,7 @@ void CheckPs5GameExampleImageClearRuntimeShape() {
scalar_code.push_back(EncodeMubuf0(0x04u, 0, true, false));
scalar_code.push_back(EncodeMubuf1(1, 0, 0));
AppendEnd(&scalar_code);
clean_scalar = true;
// The scalar input is a walker load, so it needs no specialization memory reader.
auto loaded = Compile(scalar_code);
runtime_resources = loaded;
Require("Ps5GameExampleImageClear", "scalar-loaded fill",
@@ -44788,12 +44778,6 @@ void CheckPs5GameExampleImageClearRuntimeShape() {
packed_clear, size) &&
packed_clear == scalar_clear && descriptor.Base48() == 0x200000u,
"GTA3's scalar input was confused with its destination");
clean_scalar = false;
runtime_resources = Compile(scalar_code);
Require("Ps5GameExampleImageClear", "unavailable clean scalar",
!ResolveComputeBufferFill(compute, 1, 1, 1, 0x41u, descriptor,
packed_clear, size),
"a scalar without a clean reader was replaced by a fill");
std::printf("[host] %-32s ok\n", "Ps5GameExampleImageClear");
}
@@ -47260,8 +47244,9 @@ void CheckImageSamplerSpecialization() {
auto plan = ExtractResourcePlan(translated.program);
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
const SrtRuntime runtime{.user_data = user_data, .read_memory = ReadTestMemory,
.userdata = &memory, .read_specialization_memory = ReadTestMemory};
const SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
.read_specialization_memory = ReadTestMemory};
FakeGuestMemory fake(ReadTestMemory, &memory);
Require(name, "bounded native material plan",
plan.capture_specialization_reads && !translated.program.has_address_writes &&
MaterializeResources(plan, runtime, snapshot, specialization) &&
@@ -47351,10 +47336,8 @@ void CheckResourcePlanHandoff() {
auto plan = ExtractResourcePlan(translated.program);
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
const SrtRuntime runtime{.user_data = user_data,
.read_memory = ReadTestMemory,
.userdata = &memory,
.read_specialization_memory = ReadTestMemory};
const SrtRuntime runtime{.user_data = user_data};
FakeGuestMemory fake(ReadTestMemory, &memory);
Require(name, "initial descriptors",
MaterializeResources(plan, runtime, snapshot, specialization) &&
snapshot.buffers.size() == 1 && snapshot.buffers[0].dwords[0] == 0x1000u,
+113
View File
@@ -0,0 +1,113 @@
#ifndef KYTY_TESTS_SRTWALKERTESTMEMORY_H_
#define KYTY_TESTS_SRTWALKERTESTMEMORY_H_
#include "graphics/shader/recompiler/ir/passes/SrtWalker.h"
#include <xbyak/xbyak.h>
#include <csignal>
#include <cstdlib>
#include <utility>
#include <vector>
#if defined(__linux__)
#include <ucontext.h>
#elif defined(_WIN32)
#include <windows.h>
#endif
// Serves SRT walker loads from fake guest addresses: a walker load that faults completes with
// the test reader's DWORD, or with zero like the emulator when the reader fails or is null.
class FakeGuestMemory {
public:
explicit FakeGuestMemory(Libs::Graphics::ShaderRecompiler::IR::SrtMemoryReader reader = nullptr,
void *userdata = nullptr) {
Current() = {reader, userdata};
#if defined(__linux__)
struct sigaction action {};
action.sa_sigaction = Handle;
action.sa_flags = SA_SIGINFO;
sigaction(SIGSEGV, &action, &m_previous);
#elif defined(_WIN32)
m_handler = AddVectoredExceptionHandler(1, Handle);
#endif
}
~FakeGuestMemory() {
#if defined(__linux__)
sigaction(SIGSEGV, &m_previous, nullptr);
#elif defined(_WIN32)
RemoveVectoredExceptionHandler(m_handler);
#endif
Current() = {};
}
FakeGuestMemory(const FakeGuestMemory &) = delete;
FakeGuestMemory &operator=(const FakeGuestMemory &) = delete;
private:
using Reader = std::pair<Libs::Graphics::ShaderRecompiler::IR::SrtMemoryReader, void *>;
static Reader &Current() {
static thread_local Reader reader;
return reader;
}
static uint32_t Read(uint64_t address) {
uint32_t word = 0;
const auto [reader, userdata] = Current();
return reader != nullptr && reader(userdata, address, {&word, 1}) ? word : 0u;
}
#if defined(__linux__)
static void Handle(int, siginfo_t *, void *context) {
auto &registers = static_cast<ucontext_t *>(context)->uc_mcontext.gregs;
const auto length =
Libs::Graphics::ShaderRecompiler::IR::SrtWalkerLoadLength(registers[REG_RIP]);
if (length == 0) std::abort();
registers[REG_RAX] = Read(registers[REG_R10]);
registers[REG_RIP] += length;
}
struct sigaction m_previous {};
#elif defined(_WIN32)
static LONG CALLBACK Handle(EXCEPTION_POINTERS *info) {
auto &context = *info->ContextRecord;
const auto length = Libs::Graphics::ShaderRecompiler::IR::SrtWalkerLoadLength(context.Rip);
if (info->ExceptionRecord->ExceptionCode != EXCEPTION_ACCESS_VIOLATION || length == 0)
return EXCEPTION_CONTINUE_SEARCH;
context.Rax = Read(context.R10);
context.Rip += length;
return EXCEPTION_CONTINUE_EXECUTION;
}
void *m_handler = nullptr;
#endif
};
// Evaluates one value through a walker compiled for it, keeping the plan's resource walker.
inline bool EvaluateSrtValue(Libs::Graphics::ShaderRecompiler::IR::ResourcePlan &plan,
const Libs::Graphics::ShaderRecompiler::IR::SrtRuntime &runtime,
Libs::Graphics::ShaderRecompiler::IR::Value value, uint32_t &result) {
using namespace Libs::Graphics::ShaderRecompiler::IR;
const auto *inst = value.Resolve().TryInstruction();
const auto output = inst != nullptr ? inst->WalkOutput() : UINT32_MAX;
auto code = std::move(plan.srt_code);
auto scratch = std::move(plan.srt_scratch);
const auto user_data = plan.srt_user_data;
const auto captures = plan.srt_captures;
CompileSrtWalker(plan, {&value, 1});
std::vector<uint32_t> flat;
const bool known = WalkSrt(plan, runtime, flat, nullptr) && WalkedValue(plan, value, result);
plan.srt_code = std::move(code);
plan.srt_scratch = std::move(scratch);
plan.srt_user_data = user_data;
plan.srt_captures = captures;
if (inst != nullptr) inst->SetWalkOutput(output);
return known;
}
// Walks the plan's resources and reads back one descriptor.
inline bool EvaluateSrtDescriptor(const Libs::Graphics::ShaderRecompiler::IR::ResourcePlan &plan,
const Libs::Graphics::ShaderRecompiler::IR::SrtRuntime &runtime,
uint32_t source,
Libs::Graphics::ShaderRecompiler::IR::DescriptorValue &result) {
using namespace Libs::Graphics::ShaderRecompiler::IR;
std::vector<uint32_t> flat;
return WalkSrt(plan, runtime, flat, nullptr) && WalkedDescriptor(plan, source, result);
}
#endif // KYTY_TESTS_SRTWALKERTESTMEMORY_H_
+103 -143
View File
@@ -24,13 +24,13 @@
#include "graphics/shader/recompiler/ir/passes/ResourceTracking.h"
#include "graphics/shader/recompiler/ir/passes/ResourceMaterialization.h"
#include "graphics/shader/recompiler/ir/passes/ShaderInfoCollection.h"
#include "graphics/shader/recompiler/ir/passes/SrtWalker.h"
#include "graphics/shader/recompiler/ir/passes/SsaRewrite.h"
#include "graphics/shader/shader.h"
#include "graphics/shader/shaderCompiler.h"
#include "libs/agc.h"
#include "spirv-tools/libspirv.hpp"
#include "xxhash.h"
#include "SrtWalkerTestMemory.h"
#include <algorithm>
#include <array>
@@ -135,8 +135,7 @@ struct TestCompileResult {
TestCompileResult RecompileForTest(
std::span<const uint32_t> code,
const ShaderRecompiler::CompileOptions &options,
ShaderRecompiler::IR::SrtMemoryReader read_memory = nullptr,
void *read_memory_data = nullptr, uint32_t push_data_start_dword = 0) {
uint32_t push_data_start_dword = 0) {
auto translated = ShaderRecompiler::TranslateProgram(code, options);
auto plan = ShaderRecompiler::IR::ExtractResourcePlan(translated.program);
ShaderRecompiler::IR::ResourceSnapshot resources;
@@ -144,8 +143,6 @@ TestCompileResult RecompileForTest(
const ShaderRecompiler::IR::SrtRuntime runtime{
.user_data = options.user_data,
.shader_base = reinterpret_cast<uint64_t>(code.data()),
.read_memory = read_memory,
.userdata = read_memory_data,
.read_specialization_memory = ReadHostTestMemory,
};
Check(ShaderRecompiler::IR::MaterializeResources(
@@ -1588,11 +1585,6 @@ void SetImageTestFormat(std::array<uint32_t, 64> *data, uint32_t srsrc,
(*data)[format_dword] = static_cast<uint32_t>(format) << 20u;
}
bool ReadZeroTestMemory(void *, uint64_t, std::span<uint32_t> values) {
std::ranges::fill(values, 0u);
return true;
}
constexpr uint32_t EncodeVop2(uint32_t opcode, uint32_t dst, uint32_t src0,
uint32_t src1) {
return ((opcode & 0x3fu) << 25u) | ((dst & 0xffu) << 17u) |
@@ -5408,7 +5400,8 @@ void TestNewShaderRecompilerMemoryFamilyTranslation() {
options.dump_ir = true;
options.user_data = user_data;
auto result = RecompileForTest(shader, options, ReadZeroTestMemory);
FakeGuestMemory zero_memory;
auto result = RecompileForTest(shader, options);
Check((result.decoded_dump.find("s_load_dword") != std::string::npos),
"new decoder did not decode SMEM dword load");
Check((result.decoded_dump.find("s_buffer_load_dword") != std::string::npos),
@@ -9223,7 +9216,7 @@ void TestSharedReturnPreservesDescriptorDominance() {
user_data[29] = static_cast<uint32_t>(address >> 32u);
auto options = MakeCompileOptions(ShaderType::Pixel);
options.user_data = user_data;
auto result = RecompileForTest(shader, options, ReadHostTestMemory);
auto result = RecompileForTest(shader, options);
Check(!result.program.dispatcher_fallback &&
result.resources.buffers.size() == 1u &&
result.resources.buffers[0].dwords[0] == 0x2000u,
@@ -9283,7 +9276,7 @@ void TestNestedSelectionPreservesDescriptorSources() {
auto options = MakeCompileOptions(ShaderType::Compute);
options.user_data_base = 8;
options.user_data = user_data;
const auto result = RecompileForTest(shader, options, ReadHostTestMemory);
const auto result = RecompileForTest(shader, options);
Check(!result.program.dispatcher_fallback && result.resources.buffers.size() == 4u,
"nested descriptor selection lost its structured resource bindings");
for (size_t i = 0; i < result.program.info.buffers.size(); ++i) {
@@ -9359,7 +9352,7 @@ void TestSharedExitPreservesNativeDescriptorSources() {
user_data[2] = arm;
auto options = MakeCompileOptions(ShaderType::Pixel);
options.user_data = user_data;
const auto result = RecompileForTest(shader, options, ReadHostTestMemory);
const auto result = RecompileForTest(shader, options);
Check(!result.program.dispatcher_fallback && result.resources.buffers.size() == 2u,
"shared-exit descriptor selection lost its structured resource bindings");
for (size_t i = 0; i < result.program.info.buffers.size(); ++i) {
@@ -9405,7 +9398,7 @@ void TestNativeScalarReadDescriptorPlanning() {
user_data[8] = mode;
auto options = MakeCompileOptions(ShaderType::Pixel);
options.user_data = user_data;
const auto result = RecompileForTest(shader, options, ReadHostTestMemory);
const auto result = RecompileForTest(shader, options);
const auto expected = mode == 2u ? table : std::array<uint32_t, 4>{};
Check(!result.program.dispatcher_fallback && result.resources.buffers.size() == 1u &&
std::ranges::equal(result.resources.flattened_srt, expected),
@@ -9507,7 +9500,7 @@ void TestNativeGuardedSamplerSource(bool inline_constants) {
auto options = MakeCompileOptions(ShaderType::Pixel);
options.user_data_base = 8;
options.user_data = user_data;
const auto result = RecompileForTest(code, options, ReadHostTestMemory);
const auto result = RecompileForTest(code, options);
const auto expected = mode == 2u ? sampler : std::array<uint32_t, 4>{};
Check(!result.program.dispatcher_fallback && result.resources.samplers.size() == 1u &&
result.resources.samplers[0].dword_count == expected.size() &&
@@ -9615,8 +9608,7 @@ void TestBoundedMaterialBufferStores(bool scalar_key) {
ShaderRecompiler::IR::ResourceSnapshot snapshot;
ShaderRecompiler::IR::ResourceSpecialization specialization;
const ShaderRecompiler::IR::SrtRuntime runtime{
.user_data = user_data, .read_memory = ReadHostTestMemory,
.read_specialization_memory = ReadHostTestMemory};
.user_data = user_data, .read_specialization_memory = ReadHostTestMemory};
const auto materialize = [&] {
return ShaderRecompiler::IR::MaterializeResources(plan, runtime, snapshot, specialization);
};
@@ -9701,7 +9693,7 @@ void TestNativeDescriptorProvenanceKeepsGpuSelection() {
options.input_info.compute = &input_info;
options.user_data = user_data;
for (const auto &shader : {selected, partial, loop}) {
const auto result = RecompileForTest(shader, options, ReadHostTestMemory);
const auto result = RecompileForTest(shader, options);
Check(result.program.info.uses_dma && result.program.info.buffers.size() == 1u &&
result.program.info.buffers[0].written,
"GPU-selected scalar descriptor was materialized as a host binding");
@@ -10864,7 +10856,7 @@ void TestMemoryFedScalarLeafCall() {
struct Memory {
std::array<uint32_t, 128> words{};
uint64_t end = base + 0x10c;
bool clean = true;
bool readable = true;
} memory;
std::copy_n(std::array{0xe0040060u, 0x0010000fu, 8u, 0x5204u}.begin(),
4, memory.words.begin());
@@ -10875,13 +10867,14 @@ void TestMemoryFedScalarLeafCall() {
memory.words[66] = EncodeSop1(0x21, 125, 14);
const auto read = [](void *data, uint64_t address, std::span<uint32_t> out) {
const auto& m = *static_cast<Memory*>(data);
if (!m.clean || address < base || address > m.end || out.size_bytes() > m.end - address)
if (!m.readable || address < base || address > m.end || out.size_bytes() > m.end - address)
return false;
std::memcpy(out.data(), reinterpret_cast<const uint8_t*>(m.words.data()) + address - base,
out.size_bytes());
return true;
};
IR::SrtRuntime runtime{.read_memory = read, .userdata = &memory, .read_specialization_memory = read};
FakeGuestMemory fake(read, &memory);
IR::SrtRuntime runtime{.userdata = &memory, .read_specialization_memory = read};
const auto options = MakeCompileOptions(ShaderType::Compute);
const uint32_t gpu_callable[] = {EncodeVop1(0x02, 4, 256),
EncodeSmem0(0x09, 14, 0), 4u << 25u, EncodeSop1(0x21, 14, 14), EncodeSopp(0x01)};
@@ -10965,14 +10958,14 @@ void TestMemoryFedScalarLeafCall() {
Check(source.revision == 3 && source.reads.back().second == 8,
"shorter exact-boundary leaf was not decoded after a failed old-span read");
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
memory.clean = false;
memory.readable = false;
ExpectFatal([&] { (void)RefreshShaderSource(source, runtime); },
"unreadable call dependencies reused stale linked code");
memory.clean = true;
auto ordinary = runtime;
ordinary.read_specialization_memory = nullptr;
ExpectFatal([&] { (void)RefreshShaderSource(source, ordinary); },
"call target query accepted an ordinary non-strict memory reader");
memory.readable = true;
auto no_reader = runtime;
no_reader.read_specialization_memory = nullptr;
ExpectFatal([&] { (void)RefreshShaderSource(source, no_reader); },
"leaf code was linked without a specialization memory reader");
std::vector<uint32_t> escaping(std::begin(shader), std::end(shader) - 1);
escaping.insert(escaping.end(), {EncodeVop1(0x01, 1, 14),
EncodeMubuf0(0x1c, 0, false), EncodeMubuf1(1, 0, 0), EncodeSopp(0x01)});
@@ -10988,9 +10981,10 @@ void TestMemoryFedScalarLeafCall() {
EncodeSMovB32(15, 129), EncodeSop2(0x0f, 14, 128, 128),
EncodeSop1(0x21, 14, 14), EncodeSopp(0x01)};
auto pair_source = PrepareShaderSource(high_writer, options);
IR::SrtWalker pair_walker(pair_source.call_targets, runtime);
std::vector<uint32_t> flat;
uint32_t high = 1;
Check(pair_walker.Evaluate(pair_source.calls.front().target[1], high) && high == 0,
Check(IR::WalkSrt(pair_source.call_targets, runtime, flat, nullptr) &&
IR::WalkedValue(pair_source.call_targets, pair_source.calls.front().target[1], high) && high == 0,
"scalar pair producer used a stale preceding high-half definition");
const uint32_t joined[] = {
EncodeSopp(0x04, 2), EncodeSMovB32(14, 129), EncodeSopp(0x02, 1),
@@ -11136,7 +11130,7 @@ void TestMeshExportStorage() {
options.back_code = split ? std::span{back} : std::span<const uint32_t>{};
const auto result = RecompileForTest(
split ? std::span{front} : std::span<const uint32_t>{monolithic},
options, nullptr, nullptr, push_data_start);
options, push_data_start);
CheckSpirvBinaryValidates(result.spirv);
const auto &layout = result.program.bindings;
Check(layout.UsesPushData() == (push_data_start == PushData::MeshDrawDwordCount) &&
@@ -11284,13 +11278,11 @@ void TestMergedShaderUserDataSnapshot() {
const auto plan = IR::ExtractResourcePlan(program);
for (const auto *params : {&first, &second}) {
const auto user_data = std::span(params->user_data).first(params->user_data_count);
const IR::SrtRuntime runtime{.user_data = user_data,
.read_memory = ReadHostTestMemory};
IR::SrtWalker walker(plan, runtime);
const IR::SrtRuntime runtime{.user_data = user_data};
IR::DescriptorValue front_descriptor, back_descriptor;
const auto &table = params == &first ? first_table : second_table;
Check(walker.EvaluateDescriptor(program.info.buffers[0].source, front_descriptor) &&
walker.EvaluateDescriptor(program.info.buffers[1].source, back_descriptor) &&
Check(EvaluateSrtDescriptor(plan, runtime, program.info.buffers[0].source, front_descriptor) &&
EvaluateSrtDescriptor(plan, runtime, program.info.buffers[1].source, back_descriptor) &&
std::equal(user_data.begin() + 8, user_data.end(),
front_descriptor.dwords.begin()) &&
std::equal(table.begin(), table.end(), back_descriptor.dwords.begin()),
@@ -13824,7 +13816,7 @@ void TestAutomaticObjectPrimitiveId() {
PushData::GraphicsDrawDwords(true, false) == PushData::MeshDrawDwordCount,
"graphics draw data reserved an incorrect prefix");
for (const uint32_t start : {prefix, PushData::DwordCount}) {
const auto result = RecompileForTest(shader, options, nullptr, nullptr, start);
const auto result = RecompileForTest(shader, options, start);
Check(result.program.bindings.UsesPushData() == (start == prefix),
"object ID changed shader-data push/spill selection");
Check(std::ranges::any_of(result.program.info.outputs, [](const StageOutput& output) {
@@ -13841,7 +13833,7 @@ void TestAutomaticObjectPrimitiveId() {
}
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
pixel.target_shader_mask = 0xf0000000u;
ExpectFatal([&] { (void)RecompileForTest(shader, options, nullptr, nullptr, prefix); },
ExpectFatal([&] { (void)RecompileForTest(shader, options, prefix); },
"automatic RT7 silently overlapped an explicit MRT7 export");
#endif
}
@@ -14489,33 +14481,6 @@ void CheckFlattenedReadSlots(const ShaderRecompiler::IR::Program &program,
}
}
bool ReadSrtHostDword(void *, uint64_t address, std::span<uint32_t> values) {
if (address == 0 || values.empty()) {
return false;
}
std::memcpy(values.data(), reinterpret_cast<const void *>(address), values.size_bytes());
return true;
}
struct SrtHostRange {
const uint32_t *data;
size_t count;
};
bool ReadSrtHostRangeDword(void *userdata, uint64_t address, std::span<uint32_t> values) {
const auto *range = static_cast<const SrtHostRange *>(userdata);
if (range == nullptr || range->data == nullptr || range->count == 0 || values.empty()) {
return false;
}
const auto base = reinterpret_cast<uint64_t>(range->data);
const auto size = range->count * sizeof(uint32_t);
if (address < base || values.size_bytes() > size || address - base > size - values.size_bytes()) {
return false;
}
std::memcpy(values.data(), reinterpret_cast<const void *>(address), values.size_bytes());
return true;
}
void TestTypedDescriptorRealWideMoveTranslation() {
const uint32_t shader[] = {
EncodeSop1(0x04, 0, 20), // s_mov_b64 s[0:1], s[20:21]
@@ -14581,7 +14546,7 @@ void TestComputeImageFill() {
options.input_info.compute = &compute;
options.user_data = userdata;
enum class Mutation { None, Offset, WrongAxis, VaryingValue, Predicate, ExtraStore, A16 };
const auto Run = [&](Mutation mutation, bool clean = true) {
const auto Run = [&](Mutation mutation) {
auto translated = ShaderRecompiler::TranslateProgram(shader, options);
auto &program = translated.program;
for (auto *block : program.blocks) {
@@ -14618,13 +14583,11 @@ void TestComputeImageFill() {
auto plan = ExtractResourcePlan(program);
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
Check(MaterializeResources(plan,
{.user_data = userdata, .read_memory = ReadHostTestMemory,
.read_specialization_memory = clean ? ReadHostTestMemory : nullptr},
snapshot, specialization),
// The fill value is a walker load, so it needs no specialization memory reader.
Check(MaterializeResources(plan, {.user_data = userdata}, snapshot, specialization),
"captured stencil clear did not materialize");
const auto &fill = snapshot.uniform_fill;
const bool expected = mutation == Mutation::None && clean;
const bool expected = mutation == Mutation::None;
Check((fill.kind == UniformFillKind::Image) == expected,
"image fill proof accepted an unsafe store or missed the captured clear");
if (expected) {
@@ -14634,7 +14597,6 @@ void TestComputeImageFill() {
}
};
Run(Mutation::None);
Run(Mutation::None, false);
for (const auto mutation : {Mutation::Offset, Mutation::WrongAxis, Mutation::VaryingValue,
Mutation::Predicate, Mutation::ExtraStore, Mutation::A16})
Run(mutation);
@@ -14835,27 +14797,22 @@ void TestGpuProducedWritableDescriptor() {
"native GPU-built writable descriptor acquired an unnecessary alias proof");
struct Reads {
std::array<uint32_t, 3> header{0x26c0u, 0u, 151u};
uint32_t ordinary = 0;
uint32_t strict = 0;
uint32_t count = 0;
} reads;
const SrtRuntime runtime{
.user_data = user_data,
.read_memory = +[](void *data, uint64_t address, std::span<uint32_t> words) {
if (words.size() != 1) return false;
FakeGuestMemory fake(+[](void *data, uint64_t address, std::span<uint32_t> words) {
auto &reads = *static_cast<Reads *>(data);
const uint32_t index = address == 0x300315028ull ? 0u
: address == 0x30031502cull ? 1u
: address == 0x300315074ull ? 2u : 3u;
if (index == 3u) return false;
++reads.ordinary;
uint32_t index = 0;
switch (address) {
case 0x300315028ull: index = 0u; break;
case 0x30031502cull: index = 1u; break;
case 0x300315074ull: index = 2u; break;
default: return false;
}
++reads.count;
words[0] = reads.header[index];
return true;
},
.userdata = &reads,
.read_specialization_memory = +[](void *data, uint64_t, std::span<uint32_t>) {
++static_cast<Reads *>(data)->strict;
return false; // Current GPU bytes are available only through the ordinary reader.
}};
}, &reads);
const SrtRuntime runtime{.user_data = user_data};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
const auto check = [&](uint32_t low, uint32_t high, uint32_t count, uint32_t reads_expected) {
@@ -14866,8 +14823,7 @@ void TestGpuProducedWritableDescriptor() {
snapshot.buffers[0].dwords[1] == high &&
snapshot.buffers[0].dwords[2] == count &&
snapshot.buffers[0].dwords[3] == 0x16204u &&
snapshot.specialization_reads.empty() && reads.strict == 0 &&
reads.ordinary == reads_expected,
snapshot.specialization_reads.empty() && reads.count == reads_expected,
"GPU-produced descriptor lost current scalar data or repeated its reads");
};
check(0x003176c0u, 0x00800003u, 151u, 3u);
@@ -14929,25 +14885,29 @@ void TestUniformSelectedWritableDescriptor() {
auto plan = ExtractResourcePlan(translated.program);
Check(plan.info.buffers.size() == 1 && plan.info.buffers[0].written,
"uniform address selection lost its writable buffer");
const auto read = +[](void *data, uint64_t address, std::span<uint32_t> words) {
const bool alternate = *static_cast<uint32_t *>(data) == 0;
if (words.size() != 1) return false;
// An unavailable read would complete as zero, so the reader counts them.
struct Memory { const uint32_t *flag; uint32_t unavailable = 0; } memory{&user_data[2]};
FakeGuestMemory fake(+[](void *data, uint64_t address, std::span<uint32_t> words) {
auto &memory = *static_cast<Memory *>(data);
const bool alternate = *memory.flag == 0;
bool available = false;
switch (address) {
case 0x1040: words[0] = 8u; return alternate;
case 0x1044: words[0] = 1u; return alternate;
case 0x1080: words[0] = 3u; return !alternate;
case 0x109c: words[0] = 2u; return !alternate;
case 0x1084: words[0] = 10u; return true;
default: return false;
case 0x1040: words[0] = 8u; available = alternate; break;
case 0x1044: words[0] = 1u; available = alternate; break;
case 0x1080: words[0] = 3u; available = !alternate; break;
case 0x109c: words[0] = 2u; available = !alternate; break;
case 0x1084: words[0] = 10u; available = true; break;
default: break;
}
};
const SrtRuntime runtime{.user_data = user_data, .read_memory = read,
.userdata = &user_data[2], .read_specialization_memory = read};
memory.unavailable += !available;
return available;
}, &memory);
const SrtRuntime runtime{.user_data = user_data};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
for (const uint32_t flag : {1u, 0u, 1u}) {
user_data[2] = flag;
Check(MaterializeResources(plan, runtime, snapshot, specialization),
Check(MaterializeResources(plan, runtime, snapshot, specialization) && memory.unavailable == 0,
"uniform selection evaluated an unavailable untaken scalar source");
const auto &descriptor = snapshot.buffers[0].dwords;
Check(descriptor[0] == (flag ? 0xe0u : 4u) &&
@@ -14977,18 +14937,16 @@ void TestUniformSelectedDescriptorLoadAddress() {
Check(plan.srt_reads.size() == 4u && plan.info.buffers.size() == 1u &&
plan.info.buffers[0].written && !plan.capture_specialization_reads,
"conditional scalar address duplicated or detached descriptor reads");
const auto read = +[](void *data, uint64_t address, std::span<uint32_t> words) {
FakeGuestMemory fake(+[](void *data, uint64_t address, std::span<uint32_t> words) {
const auto flag = *static_cast<uint32_t *>(data);
const uint64_t base = flag ? 0x2000u : 0x1000u;
const std::array<uint32_t, 4> descriptor{0x3000u + flag * 0x1000u,
4u << 16u, 16u, 0x16204u};
if (words.size() != 1u || address < base || address >= base + 16u ||
(address & 3u) != 0u) return false;
if (address < base || address >= base + 16u || (address & 3u) != 0u) return false;
words[0] = descriptor[(address - base) / 4u];
return true;
};
const SrtRuntime runtime{.user_data = user_data, .read_memory = read,
.userdata = &user_data[0]};
}, &user_data[0]);
const SrtRuntime runtime{.user_data = user_data};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
for (const uint32_t flag : {0u, 1u, 0u}) {
@@ -15058,7 +15016,7 @@ void TestBoundedScalarMaterialImageKeys() {
uint32_t stride = 160;
uint32_t records = 409;
uint64_t unavailable = 0;
bool clean = true;
bool readable = true;
bool mixed = false;
bool same_key = false;
uint32_t key_reads = 0;
@@ -15066,7 +15024,7 @@ void TestBoundedScalarMaterialImageKeys() {
} memory;
const auto read = +[](void *data, uint64_t address, std::span<uint32_t> words) {
auto &m = *static_cast<Memory *>(data);
if (!m.clean || (m.unavailable >= address &&
if (!m.readable || (m.unavailable >= address &&
m.unavailable - address < words.size_bytes())) return false;
if (words.size() == 1 && address >= 0x10a0u && address < 0x10c0u) {
const uint32_t descriptors[] = {0x2000u, m.stride << 16u, m.records, 0x5204u,
@@ -15101,6 +15059,7 @@ void TestBoundedScalarMaterialImageKeys() {
}
return false;
};
FakeGuestMemory fake(read, &memory);
const SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
.read_specialization_memory = read};
ResourceSnapshot snapshot;
@@ -15138,9 +15097,11 @@ void TestBoundedScalarMaterialImageKeys() {
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
memory.key_reads == 0 && memory.images == 0 && snapshot.images.size() == 2,
"empty scalar material loop retained image probes");
memory = {.clean = false};
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
"dirty scalar material proof was accepted");
// Unreadable descriptor and count reads are zero, which leaves the loop empty.
memory = {.readable = false};
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
memory.key_reads == 0 && memory.images == 0 && snapshot.images.size() == 2,
"unreadable scalar material count did not read as zero");
for (const uint64_t unavailable : {0x2094u, 0x211cu}) {
memory = {.unavailable = unavailable};
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
@@ -15218,12 +15179,14 @@ void TestTypedDescriptorRealCarryAndScalarLoads() {
std::array<uint32_t, 64> carry_user_data{};
const uint64_t shader_base = 0x00000012fffffffbull;
const uint64_t expected_pc = shader_base + 5u;
ShaderRecompiler::IR::SrtRuntime carry_runtime{carry_user_data, shader_base,
nullptr, nullptr};
ShaderRecompiler::IR::DescriptorValue carry_value;
Check(ShaderRecompiler::IR::SrtWalker(ShaderRecompiler::IR::ExtractResourcePlan(carry_ir), carry_runtime).EvaluateDescriptor(carry_source_index, carry_value) &&
carry_value.dwords[0] == static_cast<uint32_t>(expected_pc) &&
carry_value.dwords[1] == static_cast<uint32_t>(expected_pc >> 32u),
const ShaderRecompiler::IR::SrtRuntime carry_runtime{carry_user_data, shader_base};
auto carry_plan = ShaderRecompiler::IR::ExtractResourcePlan(carry_ir);
const auto &carry_words = carry_plan.descriptor_sources[carry_source_index].dwords;
uint32_t carry_low = 0, carry_high = 0;
Check(EvaluateSrtValue(carry_plan, carry_runtime, carry_words[0], carry_low) &&
EvaluateSrtValue(carry_plan, carry_runtime, carry_words[1], carry_high) &&
carry_low == static_cast<uint32_t>(expected_pc) &&
carry_high == static_cast<uint32_t>(expected_pc >> 32u),
"S_GETPC shader-base or add/addc carry evaluation was incorrect");
const uint32_t load_shader[] = {
@@ -15285,19 +15248,21 @@ void TestTypedDescriptorRealCarryAndScalarLoads() {
BuildTypedPlan(inline_sampler_shader,
static_cast<uint32_t>(std::size(inline_sampler_shader)),
inline_sampler_ir);
// The walk also reads the sampled image's user-data descriptor.
auto user_data = ImageTestUserData();
ShaderRecompiler::IR::DescriptorValue sampler;
ShaderRecompiler::IR::SrtRuntime runtime;
const ShaderRecompiler::IR::SrtRuntime runtime{user_data};
Check(inline_sampler_ir.info.samplers.size() == 1 &&
TypedDescriptorSource(inline_sampler_ir,
inline_sampler_ir.info.samplers[0].source) !=
nullptr &&
ShaderRecompiler::IR::SrtWalker(ShaderRecompiler::IR::ExtractResourcePlan(inline_sampler_ir), runtime).EvaluateDescriptor(inline_sampler_ir.info.samplers[0].source, sampler) &&
EvaluateSrtDescriptor(ShaderRecompiler::IR::ExtractResourcePlan(inline_sampler_ir), runtime,
inline_sampler_ir.info.samplers[0].source, sampler) &&
sampler.dwords[0] == 0 && sampler.dwords[1] == 0x00fff000u &&
sampler.dwords[2] == 0x09500000u && sampler.dwords[3] == 0,
"real inline sampler construction was unresolved or evaluated "
"incorrectly");
auto user_data = ImageTestUserData();
auto options = MakeCompileOptions(ShaderType::Pixel);
options.user_data = user_data;
auto result = RecompileForTest(inline_sampler_shader, options);
@@ -15325,9 +15290,8 @@ void TestSrtWalkerRealSmemTranslation() {
user_data[8] = static_cast<uint32_t>(address);
user_data[9] = static_cast<uint32_t>(address >> 32u);
std::vector<uint32_t> flat;
const ShaderRecompiler::IR::SrtRuntime runtime{user_data, 0, ReadSrtHostDword,
nullptr};
const auto walked = ShaderRecompiler::IR::SrtWalker(ShaderRecompiler::IR::ExtractResourcePlan(ir), runtime).RefreshFlatBuffer(flat);
const ShaderRecompiler::IR::SrtRuntime runtime{user_data};
const auto walked = ShaderRecompiler::IR::WalkSrt(ShaderRecompiler::IR::ExtractResourcePlan(ir), runtime, flat, nullptr);
Check(walked, "SRT walk failed");
Check(flat.size() == table.size() &&
std::equal(flat.begin(), flat.end(), table.begin()),
@@ -15353,10 +15317,9 @@ void TestSrtWalkerVccBaseTranslation() {
const auto address = reinterpret_cast<uint64_t>(table.data());
user_data[27] = static_cast<uint32_t>(address);
user_data[28] = static_cast<uint32_t>(address >> 32u);
const ShaderRecompiler::IR::SrtRuntime runtime{user_data, 0, ReadSrtHostDword,
nullptr};
const ShaderRecompiler::IR::SrtRuntime runtime{user_data};
std::vector<uint32_t> flat;
Check(ShaderRecompiler::IR::SrtWalker(ShaderRecompiler::IR::ExtractResourcePlan(ir), runtime).RefreshFlatBuffer(flat), "SRT walk failed");
Check(ShaderRecompiler::IR::WalkSrt(ShaderRecompiler::IR::ExtractResourcePlan(ir), runtime, flat, nullptr), "SRT walk failed");
Check(flat.size() == table.size() &&
std::equal(flat.begin(), flat.end(), table.begin()),
"typed SSA lost an SMEM base copied through VCC");
@@ -15382,17 +15345,16 @@ void TestSrtWalkerRealSBufferTranslation() {
user_data[9] = static_cast<uint32_t>(address >> 32u);
user_data[10] = sizeof(table);
std::vector<uint32_t> flat;
const ShaderRecompiler::IR::SrtRuntime runtime{user_data, 0, ReadSrtHostDword,
nullptr};
const ShaderRecompiler::IR::SrtRuntime runtime{user_data};
const auto plan = ShaderRecompiler::IR::ExtractResourcePlan(ir);
const auto walked = ShaderRecompiler::IR::SrtWalker(plan, runtime).RefreshFlatBuffer(flat);
const auto walked = ShaderRecompiler::IR::WalkSrt(plan, runtime, flat, nullptr);
Check(walked, "SRT walk failed");
Check(flat.size() == 4 &&
std::equal(flat.begin(), flat.end(), table.begin()),
"real S_BUFFER_LOAD walk did not align offset components independently");
user_data[10] = 3 * sizeof(uint32_t);
const auto bounds_walked = ShaderRecompiler::IR::SrtWalker(plan, runtime).RefreshFlatBuffer(flat);
const auto bounds_walked = ShaderRecompiler::IR::WalkSrt(plan, runtime, flat, nullptr);
Check(bounds_walked && flat == std::vector<uint32_t>{table[0], table[1], table[2], 0u},
"real S_BUFFER_LOAD walk did not zero only the out-of-bounds DWORD");
CheckFlattenedReadSlots(
@@ -15408,7 +15370,7 @@ void TestSrtWalkerRealSBufferTranslation() {
static_cast<uint32_t>(std::size(negative_shader)),
negative_ir);
user_data[10] = sizeof(table);
Check(!ShaderRecompiler::IR::SrtWalker(ShaderRecompiler::IR::ExtractResourcePlan(negative_ir), runtime).RefreshFlatBuffer(flat),
Check(!ShaderRecompiler::IR::WalkSrt(ShaderRecompiler::IR::ExtractResourcePlan(negative_ir), runtime, flat, nullptr),
"real S_BUFFER_LOAD walk accepted a negative immediate");
}
@@ -15443,11 +15405,9 @@ void TestScalarMemorySourcesCapturedBeforeWrites() {
if (opcode == 0x0a) {
user_data[base + 2u] = sizeof(table);
}
SrtHostRange range{table.data(), table.size()};
const ShaderRecompiler::IR::SrtRuntime runtime{
user_data, 0, ReadSrtHostRangeDword, &range};
const ShaderRecompiler::IR::SrtRuntime runtime{user_data};
std::vector<uint32_t> flat;
Check(ShaderRecompiler::IR::SrtWalker(ShaderRecompiler::IR::ExtractResourcePlan(ir), runtime).RefreshFlatBuffer(flat), "SRT walk failed");
Check(ShaderRecompiler::IR::WalkSrt(ShaderRecompiler::IR::ExtractResourcePlan(ir), runtime, flat, nullptr), "SRT walk failed");
Check(flat.size() == table.size() &&
std::equal(flat.begin(), flat.end(), table.begin()),
"overlapping scalar-memory load did not capture its sources before "
@@ -16007,7 +15967,7 @@ void TestGraphicsPushConstantPlacement() {
constexpr uint32_t PushDataStart = 7;
auto placed =
RecompileForTest(shader, options, nullptr, nullptr, PushDataStart);
RecompileForTest(shader, options, PushDataStart);
Check(placed.program.bindings.UsesPushData() &&
placed.program.bindings.push_data_start_dword == PushDataStart &&
placed.program.bindings.descriptor_counts[static_cast<size_t>(BindingKind::ShaderData)] == 0,
@@ -16016,7 +15976,7 @@ void TestGraphicsPushConstantPlacement() {
"SPIR-V push-data block did not use the canonical zero base");
CheckSpirvBinaryValidates(placed.spirv);
auto exact = RecompileForTest(shader, options, nullptr, nullptr, 28);
auto exact = RecompileForTest(shader, options, 28);
Check(exact.program.bindings.UsesPushData() &&
exact.program.bindings.push_data_start_dword == 28 &&
exact.program.bindings.ShaderDataDwords() == 4,
@@ -16026,8 +15986,8 @@ void TestGraphicsPushConstantPlacement() {
Check(cursor == 32, "exact-fit shader data advanced the cursor incorrectly");
CheckSpirvBinaryValidates(exact.spirv);
auto spill = RecompileForTest(shader, options, nullptr, nullptr, 29);
auto full = RecompileForTest(shader, options, nullptr, nullptr, 32);
auto spill = RecompileForTest(shader, options, 29);
auto full = RecompileForTest(shader, options, 32);
Check(!spill.program.bindings.UsesPushData() &&
spill.program.bindings.descriptor_counts[static_cast<size_t>(BindingKind::ShaderData)] == 1 &&
spill.program.bindings == full.program.bindings && spill.spirv == full.spirv,