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();