shader: eliminate retained SRT lookup aliases

This commit is contained in:
nmzik 2026-10-09 00:55:36 +02:00
parent e1ac893a0c
commit dbe5353f3a
9 changed files with 239 additions and 116 deletions

View File

@ -393,8 +393,10 @@ add_kyty_full_emulator_test(shader_cfg_tests "${KYTY_TESTS_DIR}/shaderCfgTests.c
add_executable(scalar_provenance_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/ScalarProvenanceTests.cpp"
"${KYTY_SOURCE_DIR}/graphics/guest_gpu/gpu_format.cpp"
"${KYTY_SOURCE_DIR}/graphics/host_gpu/hostMemory.cpp"
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/ir/passes/ReadLaneElimination.cpp"
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp"
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/ir/passes/SrtWalker.cpp"
)
target_link_libraries(scalar_provenance_tests fmt::fmt)

View File

@ -598,7 +598,6 @@ struct ResourcePlan {
std::vector<DescriptorSource> descriptor_sources;
std::vector<ResourceBlock> control_flow;
std::vector<SrtRead> srt_reads;
std::vector<uint8_t> clean_flat_slots;
bool requires_specialization_memory = false;
bool capture_specialization_reads = false;
bool srt_plan_complete = false;

View File

@ -173,32 +173,28 @@ const DescriptorSource* Source(const ResourcePlan& program, uint32_t source) {
return &program.descriptor_sources[source];
}
void MarkCleanFlatSlots(const ResourcePlan& program, const DescriptorSource* source,
std::span<uint8_t> slots, Value extra = {}) {
void MarkCleanReads(const std::unordered_set<const Inst*>& planned_reads,
const DescriptorSource* source, Value extra = {}) {
if (source == nullptr && extra.IsEmpty()) {
return;
}
std::vector<Value> pending;
std::vector<Value> pending;
if (source != nullptr) {
pending.assign(source->dwords.begin(), source->dwords.begin() + source->dword_count);
}
if (!extra.IsEmpty()) pending.push_back(extra);
std::vector<const Inst*> visited;
while (!pending.empty()) {
auto value = pending.back().Resolve();
auto* inst = pending.back().Resolve().TryInstruction();
pending.pop_back();
const auto* inst = value.TryInstruction();
if (inst == nullptr || std::ranges::find(visited, inst) != visited.end()) {
continue;
}
visited.push_back(inst);
if (inst->GetOpcode() == ValueOpcode::ReadConst) {
const auto slot = inst->Arg(1).Resolve();
if (slot.IsImmediate() && slot.GetType() == Type::U32 && slot.U32() < slots.size()) {
slots[slot.U32()] = 1u;
pending.push_back(program.srt_reads[slot.U32()].value);
}
continue;
if (planned_reads.contains(inst)) {
auto flags = inst->Flags<SrtReadFlags>();
flags.clean = 1u;
inst->SetFlags(flags);
}
for (size_t arg = 0; arg < inst->NumArgs(); arg++) {
pending.push_back(inst->Arg(arg));
@ -945,6 +941,12 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
if (source == nullptr) {
return value;
}
if (source->GetOpcode() == ValueOpcode::ReadConst) {
const auto slot = source->Arg(1).Resolve();
EXIT_IF(!slot.IsImmediate() || slot.GetType() != Type::U32 ||
slot.U32() >= program.srt_reads.size());
return Clone(program.srt_reads[slot.U32()].value);
}
if (source->GetOpcode() == ValueOpcode::ReadFirstLane &&
ValidateRuntimeValue(program, source->Arg(0))) {
// Uniform values need no new EXEC context; preserve their shared evaluation memo.
@ -965,6 +967,11 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
}
auto& target =
plan.value_storage.emplace_back(source->GetOpcode(), source->Flags<uint64_t>());
if (source->GetOpcode() == ValueOpcode::LoadAddressU32 ||
source->GetOpcode() == ValueOpcode::ReadConstBuffer ||
source->GetOpcode() == ValueOpcode::LoadBufferU32) {
target.SetFlags(SrtReadFlags {.index = source->Flags<MemoryFlags>().index});
}
cloned.emplace(source, &target);
if (source->GetOpcode() == ValueOpcode::Phi) {
for (size_t index = 0; index < source->NumArgs(); index++) {
@ -978,12 +985,15 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
return Value(&target);
};
bool capture_indirect_reads = false;
plan.descriptor_sources.reserve(program.descriptor_sources.size());
for (const auto& source: program.descriptor_sources) {
auto& target = plan.descriptor_sources.emplace_back();
target.dword_count = source.dword_count;
target.indirect_descriptor = source.indirect_descriptor;
if (target.indirect_descriptor.has_value()) {
plan.requires_specialization_memory = true;
capture_indirect_reads = true;
target.indirect_descriptor->key_count = Clone(target.indirect_descriptor->key_count);
target.indirect_descriptor->selector_first =
Clone(target.indirect_descriptor->selector_first);
@ -1034,34 +1044,39 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
for (uint32_t i = 0; i < plan.uniform_fill.fill.words; ++i) {
plan.uniform_fill.values[i] = Clone(plan.uniform_fill.values[i]);
}
plan.clean_flat_slots.resize(plan.srt_reads.size());
bool capture_indirect_reads = false;
for (const auto& source: plan.descriptor_sources) {
if (!source.indirect_descriptor.has_value()) continue;
const auto& indirect = *source.indirect_descriptor;
plan.requires_specialization_memory = true;
capture_indirect_reads = true;
if (indirect.selector)
MarkCleanFlatSlots(plan, Source(plan, indirect.selector->source), plan.clean_flat_slots);
MarkCleanFlatSlots(plan, nullptr, plan.clean_flat_slots, indirect.selector_mask);
MarkCleanFlatSlots(plan, nullptr, plan.clean_flat_slots, indirect.selector_first);
MarkCleanFlatSlots(plan, nullptr, plan.clean_flat_slots, indirect.key_count);
if (indirect.sources.empty()) {
MarkCleanFlatSlots(plan, Source(plan, indirect.table_source), plan.clean_flat_slots);
} else {
for (const auto candidate: indirect.sources) {
MarkCleanFlatSlots(plan, Source(plan, candidate), plan.clean_flat_slots);
}
}
}
plan.capture_specialization_reads |= capture_indirect_reads || !plan.control_flow.empty();
if (plan.capture_specialization_reads) {
// Every former flat alias now targets its retained raw read. Mark those reads
// on the normalized graph, so discarded Phi/EXEC dependencies stay discarded.
std::unordered_set<const Inst*> planned_reads;
for (const auto& read: plan.srt_reads) {
auto* inst = read.value.ResolveInstruction();
EXIT_IF(inst == nullptr || (inst->GetOpcode() != ValueOpcode::LoadAddressU32 &&
inst->GetOpcode() != ValueOpcode::ReadConstBuffer));
planned_reads.insert(inst);
}
for (const auto& source: plan.descriptor_sources) {
if (!source.indirect_descriptor.has_value()) continue;
const auto& indirect = *source.indirect_descriptor;
if (indirect.selector)
MarkCleanReads(planned_reads, Source(plan, indirect.selector->source));
MarkCleanReads(planned_reads, nullptr, indirect.selector_mask);
MarkCleanReads(planned_reads, nullptr, indirect.selector_first);
MarkCleanReads(planned_reads, nullptr, indirect.key_count);
if (indirect.sources.empty()) {
MarkCleanReads(planned_reads, Source(plan, indirect.table_source));
} else {
for (const auto candidate: indirect.sources) {
MarkCleanReads(planned_reads, Source(plan, candidate));
}
}
}
// Clean writable addresses prove that shader writes cannot overlap captured reads.
for (const auto& buffer: plan.info.buffers) {
if (buffer.written) MarkCleanFlatSlots(plan, Source(plan, buffer.source), plan.clean_flat_slots);
if (buffer.written) MarkCleanReads(planned_reads, Source(plan, buffer.source));
}
for (const auto& image: plan.info.images) {
if (image.written) MarkCleanFlatSlots(plan, Source(plan, image.source), plan.clean_flat_slots);
if (image.written) MarkCleanReads(planned_reads, Source(plan, image.source));
}
}
if (capture_indirect_reads) plan.resource_tracking_complete &= !program.has_address_writes;
@ -1086,7 +1101,7 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
observed.read_memory = CaptureOrdinaryRead;
}
SrtWalker clean(program, CleanRuntime(observed));
SrtWalker walker(program, observed, program.clean_flat_slots,
SrtWalker walker(program, observed,
capture_reads || program.requires_specialization_memory ? &clean : nullptr);
if (!walker.RefreshFlatBuffer(snapshot.flattened_srt)) {
return false;

View File

@ -324,10 +324,9 @@ private:
} // namespace
SrtWalker::SrtWalker(const ResourcePlan& program, const SrtRuntime& runtime,
std::span<const uint8_t> clean_flat_slots, SrtWalker* clean_evaluator,
Value active_mask)
: m_program(program), m_runtime(runtime), m_clean_flat_slots(clean_flat_slots),
m_clean_evaluator(clean_evaluator), m_active_mask(active_mask.Resolve()),
SrtWalker* clean_evaluator, Value active_mask)
: m_program(program), m_runtime(runtime), m_clean_evaluator(clean_evaluator),
m_active_mask(active_mask.Resolve()),
m_context(AcquireContext(program)) {}
SrtWalker::~SrtWalker() { --m_program.evaluation_depth; }
@ -455,7 +454,7 @@ bool SrtWalker::EvaluateExtract(const Inst& inst, uint64_t& result) {
}
bool SrtWalker::EvaluateRawRead(const Inst& inst, uint64_t& result) {
const auto flags = inst.Flags<MemoryFlags>();
const auto flags = inst.Flags<SrtReadFlags>();
if (flags.index >= m_program.memory_info.size()) {
return false;
}
@ -557,9 +556,8 @@ bool SrtWalker::EvaluateInst(const Inst& inst, uint64_t& result) {
return EvaluateWide(inst.Arg(0), result);
}
const auto clean_runtime = CleanRuntime(m_runtime);
SrtWalker clean_active(m_program, clean_runtime, {}, nullptr, inst.Arg(1));
SrtWalker active(m_program, m_runtime, m_clean_flat_slots, &clean_active,
inst.Arg(1));
SrtWalker clean_active(m_program, clean_runtime, nullptr, inst.Arg(1));
SrtWalker active(m_program, m_runtime, &clean_active, inst.Arg(1));
return active.EvaluateWide(inst.Arg(0), result);
}
case ValueOpcode::BitCastU32F32:
@ -573,25 +571,14 @@ bool SrtWalker::EvaluateInst(const Inst& inst, uint64_t& result) {
result = static_cast<uint32_t>(a) |
(static_cast<uint64_t>(static_cast<uint32_t>(b)) << 32u);
return true;
case ValueOpcode::ReadConst: {
const auto slot = inst.Arg(1).Resolve();
if (!slot.IsImmediate() || slot.GetType() != Type::U32 ||
slot.U32() >= m_program.srt_reads.size()) {
return false;
}
if (slot.U32() < m_clean_flat_slots.size() &&
m_clean_flat_slots[slot.U32()] != 0u && m_clean_evaluator != nullptr) {
return m_clean_evaluator->EvaluateWide(m_program.srt_reads[slot.U32()].value,
result);
}
return EvaluateWide(m_program.srt_reads[slot.U32()].value, result);
}
case ValueOpcode::LoadAddressU32:
case ValueOpcode::ReadConstBuffer:
case ValueOpcode::LoadBufferU32:
if (IsRawRead(m_program, inst)) {
if (inst.GetOpcode() == ValueOpcode::LoadBufferU32 && m_clean_evaluator != nullptr &&
m_clean_evaluator->m_active_mask == m_active_mask) {
if (m_clean_evaluator != nullptr &&
(inst.Flags<SrtReadFlags>().clean != 0u ||
(inst.GetOpcode() == ValueOpcode::LoadBufferU32 &&
m_clean_evaluator->m_active_mask == m_active_mask))) {
return m_clean_evaluator->EvaluateWide(Value(const_cast<Inst*>(&inst)), result);
}
return EvaluateRawRead(inst, result);
@ -934,8 +921,8 @@ bool SrtWalker::RefreshFlatBuffer(std::vector<uint32_t>& flat) {
const auto refresh = [&](uint32_t slot) {
if (slot >= m_program.srt_reads.size()) return false;
const auto& read = m_program.srt_reads[slot];
const bool clean = read.flat_offset < m_clean_flat_slots.size() &&
m_clean_flat_slots[read.flat_offset] != 0u;
const auto* inst = read.value.ResolveInstruction();
const bool clean = inst != nullptr && inst->Flags<SrtReadFlags>().clean != 0u;
if (clean && (m_clean_evaluator == nullptr || m_runtime.read_specialization_memory == nullptr))
return false;
auto& evaluator = clean ? *m_clean_evaluator : *this;

View File

@ -20,6 +20,13 @@ struct SrtRuntime {
std::span<const uint32_t> workgroup_counts;
};
// Retained reads no longer need the guest instruction PC. A clean read evaluates
// its address and bounds through the strict reader as well as the final DWORD.
struct SrtReadFlags {
uint32_t index = 0;
uint32_t clean = 0;
};
enum class RuntimeValueType { Any, Integer };
bool ValidateRuntimeValue(const ResourcePlan& program, Value value,
@ -31,8 +38,7 @@ SrtRuntime CleanRuntime(SrtRuntime runtime);
class SrtWalker {
public:
SrtWalker(const ResourcePlan& program, const SrtRuntime& runtime,
std::span<const uint8_t> clean_flat_slots = {}, SrtWalker* clean_evaluator = nullptr,
Value active_mask = {});
SrtWalker* clean_evaluator = nullptr, Value active_mask = {});
~SrtWalker();
SrtWalker(const SrtWalker&) = delete;
SrtWalker& operator=(const SrtWalker&) = delete;
@ -54,7 +60,6 @@ private:
const ResourcePlan& m_program;
SrtRuntime m_runtime;
std::span<const uint8_t> m_clean_flat_slots;
SrtWalker* m_clean_evaluator = nullptr;
Value m_active_mask;
ResourcePlan::EvaluationContext& m_context;

View File

@ -32,7 +32,7 @@ AddValueBlock(Libs::Graphics::ShaderRecompiler::IR::Program &program) {
return *result;
}
Libs::Graphics::ShaderRecompiler::IR::ResourcePlan SrtPlan(uint64_t address) {
Libs::Graphics::ShaderRecompiler::IR::Program SrtProgram(uint64_t address) {
using namespace Libs::Graphics::ShaderRecompiler::IR;
Program program;
program.stage = Libs::Graphics::ShaderType::Compute;
@ -62,7 +62,7 @@ Libs::Graphics::ShaderRecompiler::IR::ResourcePlan SrtPlan(uint64_t address) {
source.dwords[1] = Value(0u);
source.dword_count = 2;
program.descriptor_sources.push_back(source);
return ExtractResourcePlan(program);
return program;
}
Libs::Graphics::ShaderRecompiler::IR::ResourcePlan UnbasedFlatPlan() {
@ -152,7 +152,7 @@ Libs::Graphics::ShaderRecompiler::IR::Program MixedSamplerProgram() {
void TestMappedSrtUsesDirectReaderByDefault() {
using namespace Libs::Graphics::ShaderRecompiler::IR;
const uint32_t dword = 0x12345678;
auto plan = SrtPlan(reinterpret_cast<uint64_t>(&dword));
auto plan = ExtractResourcePlan(SrtProgram(reinterpret_cast<uint64_t>(&dword)));
uint32_t specialization_reads = 0;
const SrtRuntime runtime{.userdata = &specialization_reads,
.read_specialization_memory =
@ -170,7 +170,7 @@ void TestMappedSrtUsesDirectReaderByDefault() {
void TestIntegerRuntimeValueFollowsSrtReads() {
using namespace Libs::Graphics::ShaderRecompiler::IR;
auto plan = SrtPlan(0x10000);
auto plan = SrtProgram(0x10000);
const auto root = plan.descriptor_sources.front().dwords[0];
Check(ValidateRuntimeValue(plan, root, RuntimeValueType::Integer),
"integer SRT read was rejected");
@ -217,6 +217,98 @@ void TestIntegerRuntimeValueFollowsSrtReads() {
"cyclic SRT read-first-lane dependency was accepted");
}
void TestSrtAliasesRetainReadPolicy() {
using namespace Libs::Graphics::ShaderRecompiler::IR;
auto program = SrtProgram(0x1000u);
auto &block = *program.blocks[0];
const auto append_read = [&](uint32_t address) {
auto &handle = block.AppendNewInst(ValueOpcode::GetAddressResource,
{Value(address), Value(0u)});
auto &read = block.AppendNewInst(ValueOpcode::LoadAddressU32,
{Value(&handle), Value(0u), Value(0u), Value(true)});
// Different reads deliberately share MemoryInfo but have different guest PCs.
read.SetFlags(MemoryFlags{.index = 0, .pc = address});
const auto slot = static_cast<uint32_t>(program.srt_reads.size());
program.srt_reads.push_back({Value(&read), slot});
auto &srt = block.AppendNewInst(ValueOpcode::GetSrtResource);
return Value(&block.AppendNewInst(ValueOpcode::ReadConst,
{Value(&srt), Value(slot)}));
};
const auto ordinary = append_read(0x2000u);
const auto pointer = append_read(0x3000u);
program.srt_reads[0].value.Instruction()->Arg(0).Instruction()->SetArg(0, pointer);
auto &mask = block.AppendNewInst(ValueOpcode::INotEqual32, {ordinary, Value(0u)});
auto &first = block.AppendNewInst(ValueOpcode::ReadFirstLane,
{Value(9u), Value(&mask)});
program.descriptor_sources.push_back({.dwords = {Value(&first), Value(0u)},
.dword_count = 2});
DescriptorSource indirect;
indirect.indirect_descriptor.emplace().sources = {0, 1};
program.descriptor_sources.push_back(indirect);
auto plan = ExtractResourcePlan(program);
for (const auto &inst : plan.value_storage) {
Check(inst.GetOpcode() != ValueOpcode::ReadConst &&
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");
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) {
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;
else return false;
return true;
}};
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");
}
}
void TestUniformVectorDescriptorRead() {
using namespace Libs::Graphics::ShaderRecompiler::IR;
Program program;
@ -285,16 +377,24 @@ void TestUniformVectorDescriptorRead() {
}
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;
uint32_t result = 99;
Check(SrtWalker(program, runtime).Evaluate(Value(&inactive), result) &&
result == 0 && reads.strict == 0 && reads.ordinary == 0,
"literal false EXEC read vector memory");
{
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,
"literal false EXEC read vector memory");
}
count.SetArg(4, Value(true));
handle.SetArg(3, Value(0x204u));
Check(SrtWalker(program, runtime).Evaluate(Value(&count), result) &&
result == 0 && reads.strict == 0 && reads.ordinary == 0,
"invalid vector buffer format read memory");
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,
"invalid vector buffer format read memory");
}
handle.SetArg(3, Value(0x16204u));
count.SetArg(1, Value(&lane));
Check(!ValidateRuntimeValue(program, Value(&count)),
@ -567,6 +667,7 @@ void DbgExit(int) { std::abort(); }
int main() {
TestMappedSrtUsesDirectReaderByDefault();
TestIntegerRuntimeValueFollowsSrtReads();
TestSrtAliasesRetainReadPolicy();
TestUniformVectorDescriptorRead();
TestExactReciprocalDescriptorArithmetic();
TestUnbasedFlatCacheHitMaterializes();

View File

@ -1758,17 +1758,18 @@ void TestRuntimeUnsignedMinDescriptor() {
{descriptor, Value(0u), Value(0u), Value(0u), Value(true)},
fixture.AddMemory(memory, 0x330));
fixture.PlanAndTrack();
auto plan = ExtractResourcePlan(fixture.program);
std::array<uint32_t, 1> user_data{0xffffffffu};
SrtRuntime runtime{.user_data = user_data};
DescriptorValue value;
const auto source = fixture.program.info.buffers[0].source;
Check(SrtWalker(fixture.program, runtime).EvaluateDescriptor(source, value) &&
Check(SrtWalker(plan, runtime).EvaluateDescriptor(source, value) &&
value.dwords[3] == 0x100u,
"runtime descriptor unsigned minimum did not clamp its first operand");
user_data[0] = 0x80u;
Check(
SrtWalker(fixture.program, runtime).EvaluateDescriptor(source, value) &&
SrtWalker(plan, runtime).EvaluateDescriptor(source, value) &&
value.dwords[3] == 0x80u,
"runtime descriptor unsigned minimum did not preserve its first operand");
}
@ -1875,6 +1876,7 @@ void TestSampleAdjustSamplerScratch() {
{image, shift_sampler, fixture.ImageAddress()},
fixture.AddMemory(memory, 0x200));
fixture.PlanAndTrack();
auto plan = ExtractResourcePlan(fixture.program);
const auto source = fixture.program.info.samplers[0].source;
const auto stored = fixture.program.descriptor_sources[source]
@ -1886,12 +1888,12 @@ void TestSampleAdjustSamplerScratch() {
std::array<uint32_t, 4> user_data{4u, 1u, 2u, 0x80000abcu};
SrtRuntime runtime{.user_data = user_data};
DescriptorValue descriptor;
Check(SrtWalker(fixture.program, runtime).EvaluateDescriptor(source, descriptor) &&
Check(SrtWalker(plan, runtime).EvaluateDescriptor(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(fixture.program, runtime).EvaluateDescriptor(shift_source, descriptor) &&
Check(SrtWalker(plan, runtime).EvaluateDescriptor(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");
@ -2239,6 +2241,7 @@ void TestSrtFlatteningAndRuntimeMemoization() {
SrtRuntime runtime{.user_data = user_data,
.read_memory = ReadTestMemory,
.userdata = &memory};
auto plan = ExtractResourcePlan(fixture.program);
DescriptorValue descriptor;
std::vector<uint32_t> flat;
const uint32_t request = fixture.program.info.buffers[0].source;
@ -2246,27 +2249,25 @@ void TestSrtFlatteningAndRuntimeMemoization() {
SrtWalker walker(plan, runtime);
return walker.EvaluateDescriptor(request, descriptor) && walker.RefreshFlatBuffer(flat);
};
Check(refresh(fixture.program), "typed runtime source evaluation failed");
Check(refresh(plan), "typed runtime source evaluation failed");
Check(descriptor.dwords[0] == 0xdeadbeefu &&
flat == std::vector<uint32_t>{0xdeadbeefu} && memory.reads == 1,
"descriptor and flat SRT evaluation did not share one memoized read");
memory.reads = 0;
memory.words[1] = 0x12345678u;
Check(refresh(fixture.program) && descriptor.dwords[0] == 0x12345678u &&
Check(refresh(plan) && descriptor.dwords[0] == 0x12345678u &&
flat == std::vector<uint32_t>{0x12345678u} && memory.reads == 1,
"repeated runtime evaluation reused stale scalar memory");
memory.reads = 0;
memory.fail_after = 0;
Check(!refresh(fixture.program), "unavailable scalar memory was accepted");
Check(!refresh(plan), "unavailable scalar memory was accepted");
memory.fail_after = UINT32_MAX;
Check(refresh(fixture.program) && descriptor.dwords[0] == 0x12345678u && memory.reads == 1,
Check(refresh(plan) && descriptor.dwords[0] == 0x12345678u && memory.reads == 1,
"failed runtime evaluation left a value marked as visiting");
auto detached = ExtractResourcePlan(fixture.program);
Check(refresh(detached), "detached resource plan did not evaluate");
auto moved = std::move(detached);
auto moved = std::move(plan);
memory.reads = 0;
memory.words[1] = 0x87654321u;
Check(refresh(moved) && descriptor.dwords[0] == 0x87654321u && memory.reads == 1,
@ -2296,6 +2297,7 @@ void TestDynamicSrtReadRemainsExplicit() {
{descriptor, Value(0u), Value(0u), Value(0u), Value(true)},
fixture.AddMemory(buffer, 8));
fixture.PlanAndTrack();
auto plan = ExtractResourcePlan(fixture.program);
Check(fixture.program.srt_reads.empty() &&
read.ResolveInstruction()->GetOpcode() == ValueOpcode::LoadAddressU32 &&
@ -2309,7 +2311,7 @@ void TestDynamicSrtReadRemainsExplicit() {
.read_memory = ReadTestMemory,
.userdata = &memory};
DescriptorValue value;
Check(SrtWalker(fixture.program, runtime).EvaluateDescriptor(fixture.program.info.buffers[0].source, value) &&
Check(SrtWalker(plan, runtime).EvaluateDescriptor(fixture.program.info.buffers[0].source, value) &&
value.dwords[0] == 0xabcdef01u && memory.reads == 1,
"dynamic typed scalar descriptor source was not evaluated");
@ -2475,7 +2477,7 @@ void TestConditionalSamplerPhi() {
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(SrtWalker(plan, runtime, &clean).EvaluateDescriptor(source, selected),
"conditional sampler did not survive detached plan lifetime");
for (uint32_t word = 0; word < 4; ++word) {
Check(selected.dwords[word] == memory.words[first + word],
@ -2487,13 +2489,13 @@ void TestConditionalSamplerPhi() {
no_clean_reader.read_specialization_memory = nullptr;
{
SrtWalker clean(plan, CleanRuntime(no_clean_reader));
Check(!SrtWalker(plan, no_clean_reader, {}, &clean).EvaluateDescriptor(source, selected),
Check(!SrtWalker(plan, no_clean_reader, &clean).EvaluateDescriptor(source, selected),
"conditional sampler used unchecked memory for its predicate");
}
memory.fail_address = 0x2000 + 196;
{
SrtWalker clean(plan, CleanRuntime(runtime));
Check(!SrtWalker(plan, runtime, {}, &clean).EvaluateDescriptor(source, selected),
Check(!SrtWalker(plan, runtime, &clean).EvaluateDescriptor(source, selected),
"conditional sampler ignored unavailable coherent predicate memory");
}
}
@ -2544,6 +2546,7 @@ void TestFiniteImagePhiCycle() {
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress()},
fixture.AddMemory(memory, 4));
fixture.PlanAndTrack();
auto plan = ExtractResourcePlan(fixture.program);
const auto source = fixture.program.info.images[0].source;
const auto &finite = fixture.program.descriptor_sources[source].indirect_descriptor;
Check(finite && finite->sources.size() == 2,
@ -2558,7 +2561,7 @@ 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(fixture.program, SrtRuntime{.user_data = data});
SrtWalker walker(plan, SrtRuntime{.user_data = data});
for (uint32_t candidate = 0; candidate < 2; ++candidate) {
DescriptorValue descriptor;
Check(walker.EvaluateDescriptor(finite->sources[candidate], descriptor),
@ -2658,10 +2661,11 @@ void TestLoopCycleEnteredThroughRuntimeValue() {
ConstantPropagationPass(fixture.program.blocks);
RemoveIdentities(fixture.program.blocks);
fixture.PlanAndTrack();
auto plan = ExtractResourcePlan(fixture.program);
const std::array<uint32_t, 1> user_data{0x4000u};
DescriptorValue descriptor;
Check(fixture.program.info.buffers.size() == 1u &&
SrtWalker(fixture.program, {.user_data = user_data}).EvaluateDescriptor(
SrtWalker(plan, {.user_data = user_data}).EvaluateDescriptor(
fixture.program.info.buffers[0].source, descriptor) &&
descriptor.dwords[0] == user_data[0],
"runtime-rooted invariant loop lost its buffer source");
@ -2687,11 +2691,12 @@ void TestInvariantLoopPhi() {
{handle, Value(0u), Value(0u), Value(0u), Value(true)},
fixture.AddMemory(memory, 4), loop);
fixture.PlanAndTrack();
auto plan = ExtractResourcePlan(fixture.program);
std::array<uint32_t, 1> user_data{0x12345678u};
SrtRuntime runtime{.user_data = user_data};
DescriptorValue descriptor;
Check(SrtWalker(fixture.program, runtime).EvaluateDescriptor(fixture.program.info.buffers[0].source, descriptor) &&
Check(SrtWalker(plan, runtime).EvaluateDescriptor(fixture.program.info.buffers[0].source, descriptor) &&
descriptor.dwords[0] == user_data[0],
"loop-invariant descriptor phi was not evaluated through typed SSA");
}
@ -2799,10 +2804,11 @@ void TestBoundedRelativeRegisterWrites() {
continue;
}
fixture.PlanAndTrack();
auto plan = ExtractResourcePlan(fixture.program);
const std::array<uint32_t, 3> user_data{0x1000u, 0u, 72u};
SrtRuntime runtime{.user_data = user_data};
DescriptorValue descriptor;
Check(SrtWalker(fixture.program, runtime).EvaluateDescriptor(
Check(SrtWalker(plan, runtime).EvaluateDescriptor(
fixture.program.info.buffers[0].source, descriptor) &&
descriptor.dwords[2] == 72u,
"impossible relative writes left a false descriptor dependency");

View File

@ -3,6 +3,7 @@
#include "graphics/shader/recompiler/ir/passes/DeadCodeElimination.h"
#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 <cstdlib>
@ -131,7 +132,7 @@ void TestImmediateFlatteningAndGvn() {
TestMemory memory_image{{{0x1020u, 0xfeedbeefu}}};
SrtRuntime runtime{.read_memory = ReadMemory, .userdata = &memory_image};
std::vector<uint32_t> flat;
Check(SrtWalker(fixture.program, runtime).RefreshFlatBuffer(flat), "flattened SRT walk failed");
Check(SrtWalker(ExtractResourcePlan(fixture.program), runtime).RefreshFlatBuffer(flat), "flattened SRT walk failed");
Check(flat == std::vector<uint32_t>{0xfeedbeefu} && memory_image.reads == 1,
"flattened SRT did not evaluate its canonical read once");
}
@ -147,7 +148,7 @@ void TestRawScalarComponentAlignment() {
TestMemory memory_image{{{0x1000u, 0x12345678u}}};
SrtRuntime runtime{.read_memory = ReadMemory, .userdata = &memory_image};
std::vector<uint32_t> flat;
Check(SrtWalker(fixture.program, runtime).RefreshFlatBuffer(flat), "raw scalar SRT walk failed");
Check(SrtWalker(ExtractResourcePlan(fixture.program), runtime).RefreshFlatBuffer(flat), "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");
@ -202,7 +203,7 @@ void TestNestedSrtWalk() {
TestMemory memory_image{{{0x1000u, 0x2000u}, {0x2000u, 0xabcdef01u}}};
SrtRuntime runtime{.read_memory = ReadMemory, .userdata = &memory_image};
std::vector<uint32_t> flat;
Check(SrtWalker(fixture.program, runtime).RefreshFlatBuffer(flat), "nested SRT walk failed");
Check(SrtWalker(ExtractResourcePlan(fixture.program), runtime).RefreshFlatBuffer(flat), "nested SRT walk failed");
Check(flat == std::vector<uint32_t>({0x2000u, 0xabcdef01u}),
"nested typed SRT reads were not evaluated in dependency order");
}
@ -225,7 +226,7 @@ void TestShaderBaseAndUserData() {
SrtRuntime runtime{.user_data = user_data,
.shader_base = 0x12345678abcdef00ull};
DescriptorValue result;
Check(SrtWalker(fixture.program, runtime).EvaluateDescriptor(0, result),
Check(SrtWalker(ExtractResourcePlan(fixture.program), runtime).EvaluateDescriptor(0, result),
"shader-relative descriptor evaluation failed");
Check(result.dword_count == 3 && result.dwords[0] == 0xabcdef00u &&
result.dwords[1] == 0x12345678u && result.dwords[2] == 0x24u,
@ -250,7 +251,7 @@ void TestCarryAndBitFields() {
{.dwords = {low, high, inserted, sign}, .dword_count = 4});
DescriptorValue result;
Check(SrtWalker(fixture.program, {}).EvaluateDescriptor(0, result),
Check(SrtWalker(ExtractResourcePlan(fixture.program), {}).EvaluateDescriptor(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,11 +274,12 @@ void TestInvariantAndDivergentPhi() {
fixture.program.descriptor_sources.push_back(
{.dwords = {Value(&divergent)}, .dword_count = 1});
const auto plan = ExtractResourcePlan(fixture.program);
DescriptorValue result;
Check(SrtWalker(fixture.program, {}).EvaluateDescriptor(0, result) &&
Check(SrtWalker(plan, {}).EvaluateDescriptor(0, result) &&
result.dwords[0] == 7u,
"loop-invariant typed phi was rejected");
Check(!SrtWalker(fixture.program, {}).EvaluateDescriptor(1, result),
Check(!SrtWalker(plan, {}).EvaluateDescriptor(1, result),
"divergent phi was accepted");
}
@ -374,7 +376,7 @@ void TestRuntime64BitDescriptorOps() {
{.dwords = {low, high}, .dword_count = 2});
DescriptorValue result;
Check(SrtWalker(fixture.program, {}).EvaluateDescriptor(0, result) &&
Check(SrtWalker(ExtractResourcePlan(fixture.program), {}).EvaluateDescriptor(0, result) &&
result.dwords[0] == 0xabcdu && result.dwords[1] == 0x1235u,
"64-bit typed descriptor arithmetic evaluation is incorrect");
}
@ -447,12 +449,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);
DescriptorValue result;
Check(SrtWalker(fixture.program, runtime).EvaluateDescriptor(0, result) &&
Check(SrtWalker(plan, runtime).EvaluateDescriptor(0, result) &&
result.dwords[0] == 0x00300300u,
"uniform sampler LOD evaluated incorrectly");
user_data[6] = 20u;
Check(SrtWalker(fixture.program, runtime).EvaluateDescriptor(0, result) &&
Check(SrtWalker(plan, runtime).EvaluateDescriptor(0, result) &&
result.dwords[0] == 0x00ffffffu,
"uniform sampler LOD clamp evaluated incorrectly");
}
@ -487,11 +490,12 @@ void TestFloatComparisonDescriptorInputs() {
fixture.program.descriptor_sources.push_back(
{.dwords = {low, high}, .dword_count = 2});
const 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(fixture.program, {.user_data = user_data})
Check(SrtWalker(plan, {.user_data = user_data})
.EvaluateDescriptor(0, result) &&
result.dwords[0] == (flush && index < 4 ? 1 : input.less_equal) &&
result.dwords[1] == (flush && index < 4 ? 1 : input.greater_equal),
@ -566,7 +570,7 @@ void TestConstantBufferBounds() {
TestMemory memory_image{{{0x3000u, 0x12345678u}, {0x300cu, 0xa5a5a5a5u}}};
SrtRuntime runtime{.read_memory = ReadMemory, .userdata = &memory_image};
std::vector<uint32_t> flat;
Check(SrtWalker(fixture.program, runtime).RefreshFlatBuffer(flat) == test.valid,
Check(SrtWalker(ExtractResourcePlan(fixture.program), runtime).RefreshFlatBuffer(flat) == test.valid,
"constant-buffer walk misaligned or wrapped its offset components");
Check(test.valid ? flat == std::vector<uint32_t>{test.expected} : memory_image.reads == 0,
"constant-buffer walk read the wrong word or accessed an out-of-bounds address");
@ -690,7 +694,7 @@ void TestUndefinedRuntimeValueFails() {
fixture.program.descriptor_sources.push_back(
{.dwords = {undef}, .dword_count = 1});
DescriptorValue result;
Check(!SrtWalker(fixture.program, {}).EvaluateDescriptor(0, result),
Check(!SrtWalker(ExtractResourcePlan(fixture.program), {}).EvaluateDescriptor(0, result),
"undefined typed descriptor source was accepted");
}

View File

@ -21,6 +21,7 @@
#include "graphics/shader/recompiler/ir/passes/DeadCodeElimination.h"
#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/ShaderInfoCollection.h"
#include "graphics/shader/recompiler/ir/passes/SrtWalker.h"
#include "graphics/shader/recompiler/ir/passes/SsaRewrite.h"
@ -10564,14 +10565,16 @@ void TestMergedShaderUserDataSnapshot() {
const auto &program = translated.program;
Check(program.info.buffers.size() == 2 && program.srt_reads.size() == 4,
"merged shader lost front user SGPRs or the back-stage SRT load");
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);
IR::DescriptorValue front_descriptor, back_descriptor;
const auto &table = params == &first ? first_table : second_table;
Check(IR::SrtWalker(program, runtime).EvaluateDescriptor(program.info.buffers[0].source, front_descriptor) &&
IR::SrtWalker(program, runtime).EvaluateDescriptor(program.info.buffers[1].source, back_descriptor) &&
Check(walker.EvaluateDescriptor(program.info.buffers[0].source, front_descriptor) &&
walker.EvaluateDescriptor(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()),
@ -13743,7 +13746,7 @@ void TestTypedDescriptorRealCarryAndScalarLoads() {
ShaderRecompiler::IR::SrtRuntime carry_runtime{carry_user_data, shader_base,
nullptr, nullptr};
ShaderRecompiler::IR::DescriptorValue carry_value;
Check(ShaderRecompiler::IR::SrtWalker(carry_ir, carry_runtime).EvaluateDescriptor(carry_source_index, 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),
"S_GETPC shader-base or add/addc carry evaluation was incorrect");
@ -13813,7 +13816,7 @@ void TestTypedDescriptorRealCarryAndScalarLoads() {
TypedDescriptorSource(inline_sampler_ir,
inline_sampler_ir.info.samplers[0].source) !=
nullptr &&
ShaderRecompiler::IR::SrtWalker(inline_sampler_ir, runtime).EvaluateDescriptor(inline_sampler_ir.info.samplers[0].source, sampler) &&
ShaderRecompiler::IR::SrtWalker(ShaderRecompiler::IR::ExtractResourcePlan(inline_sampler_ir), runtime).EvaluateDescriptor(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 "
@ -13849,7 +13852,7 @@ void TestSrtWalkerRealSmemTranslation() {
std::vector<uint32_t> flat;
const ShaderRecompiler::IR::SrtRuntime runtime{user_data, 0, ReadSrtHostDword,
nullptr};
const auto walked = ShaderRecompiler::IR::SrtWalker(ir, runtime).RefreshFlatBuffer(flat);
const auto walked = ShaderRecompiler::IR::SrtWalker(ShaderRecompiler::IR::ExtractResourcePlan(ir), runtime).RefreshFlatBuffer(flat);
Check(walked, "SRT walk failed");
Check(flat.size() == table.size() &&
std::equal(flat.begin(), flat.end(), table.begin()),
@ -13878,7 +13881,7 @@ void TestSrtWalkerVccBaseTranslation() {
const ShaderRecompiler::IR::SrtRuntime runtime{user_data, 0, ReadSrtHostDword,
nullptr};
std::vector<uint32_t> flat;
Check(ShaderRecompiler::IR::SrtWalker(ir, runtime).RefreshFlatBuffer(flat), "SRT walk failed");
Check(ShaderRecompiler::IR::SrtWalker(ShaderRecompiler::IR::ExtractResourcePlan(ir), runtime).RefreshFlatBuffer(flat), "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");
@ -13906,14 +13909,15 @@ void TestSrtWalkerRealSBufferTranslation() {
std::vector<uint32_t> flat;
const ShaderRecompiler::IR::SrtRuntime runtime{user_data, 0, ReadSrtHostDword,
nullptr};
const auto walked = ShaderRecompiler::IR::SrtWalker(ir, runtime).RefreshFlatBuffer(flat);
const auto plan = ShaderRecompiler::IR::ExtractResourcePlan(ir);
const auto walked = ShaderRecompiler::IR::SrtWalker(plan, runtime).RefreshFlatBuffer(flat);
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(ir, runtime).RefreshFlatBuffer(flat);
const auto bounds_walked = ShaderRecompiler::IR::SrtWalker(plan, runtime).RefreshFlatBuffer(flat);
Check(!bounds_walked, "real S_BUFFER_LOAD walk ignored descriptor bounds");
CheckFlattenedReadSlots(
ir, 4, "real S_BUFFER_LOAD patch used the wrong flat offsets");
@ -13928,7 +13932,7 @@ void TestSrtWalkerRealSBufferTranslation() {
static_cast<uint32_t>(std::size(negative_shader)),
negative_ir);
user_data[10] = sizeof(table);
Check(!ShaderRecompiler::IR::SrtWalker(negative_ir, runtime).RefreshFlatBuffer(flat),
Check(!ShaderRecompiler::IR::SrtWalker(ShaderRecompiler::IR::ExtractResourcePlan(negative_ir), runtime).RefreshFlatBuffer(flat),
"real S_BUFFER_LOAD walk accepted a negative immediate");
}
@ -13967,7 +13971,7 @@ void TestScalarMemorySourcesCapturedBeforeWrites() {
const ShaderRecompiler::IR::SrtRuntime runtime{
user_data, 0, ReadSrtHostRangeDword, &range};
std::vector<uint32_t> flat;
Check(ShaderRecompiler::IR::SrtWalker(ir, runtime).RefreshFlatBuffer(flat), "SRT walk failed");
Check(ShaderRecompiler::IR::SrtWalker(ShaderRecompiler::IR::ExtractResourcePlan(ir), runtime).RefreshFlatBuffer(flat), "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 "