shader: respect branch reachability when refreshing scalar resources

This commit is contained in:
nmzik
2026-10-01 18:42:53 +04:00
parent 0d0b215fab
commit 6409be2874
12 changed files with 342 additions and 122 deletions
+118 -6
View File
@@ -1386,11 +1386,17 @@ void TestUniformizedMaterialImageKeys() {
specialization.images[0].indirect_mapping_offset + 3u] == 4096u,
"material mask did not limit sparse descriptor reads");
user_data[8] = first_material;
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
"written buffer alias with a material key was accepted");
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
std::ranges::find(snapshot.specialization_reads,
std::pair<uint64_t, uint64_t>{first_material, 4}) !=
snapshot.specialization_reads.end(),
"material key read was omitted from the renderer write-overlap check");
user_data[8] = first_table;
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
"written buffer alias with an image record was accepted");
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
std::ranges::find(snapshot.specialization_reads,
std::pair<uint64_t, uint64_t>{first_table, 32}) !=
snapshot.specialization_reads.end(),
"image descriptor read was omitted from the renderer write-overlap check");
CheckFatal([&] { make_plan(Variant::WrongUpdate); }, "not a valid runtime value",
"non-clearing material mask was accepted");
CheckFatal([&] { make_plan(Variant::WrongEquality); }, "not a valid runtime value",
@@ -2785,6 +2791,108 @@ void TestConditionalBufferMaterialization() {
CheckActive();
}
void TestGuardedScalarDescriptorReads() {
namespace CFG = Libs::Graphics::ShaderRecompiler::CFG;
for (const bool shared : {false, true}) {
for (const bool exec : {false, true}) {
Fixture fixture;
auto *entry = fixture.block;
auto *optional = fixture.AddBlock();
auto *done = fixture.AddBlock();
entry->AddBranch(optional);
entry->AddBranch(done);
optional->AddBranch(done);
fixture.program.block_info[0].terminator = {
.kind = CFG::TerminatorKind::ConditionalBranch, .true_block = 1, .false_block = 2};
fixture.program.block_info[1].terminator = {
.kind = CFG::TerminatorKind::Branch, .true_block = 2};
fixture.program.block_info[2].terminator.kind = CFG::TerminatorKind::Return;
const auto control = fixture.Buffer({fixture.UserData(0), fixture.UserData(1),
fixture.UserData(2), fixture.UserData(3)});
MemoryInfo scalar;
scalar.kind = ResourceKind::ScalarBuffer;
scalar.offset = 28;
const auto flag = fixture.Emit(ValueOpcode::ReadConstBuffer, {control, Value(0u)},
fixture.AddMemory(scalar, 4));
const auto lane = fixture.Emit(ValueOpcode::GetBuiltin,
{Value(static_cast<uint32_t>(StageInputKind::LocalInvocationId)), Value(0u)});
const auto varying = fixture.Emit(ValueOpcode::INotEqual32, {lane, Value(0u)});
const auto enabled = fixture.Emit(ValueOpcode::INotEqual32, {flag, Value(0u)});
fixture.program.block_info[0].condition =
fixture.Emit(ValueOpcode::LogicalAnd, {varying, enabled});
const auto root = fixture.Address(fixture.UserData(4), Value(0u));
const auto Load = [&](Block *block) {
fixture.block = block;
MemoryInfo address;
address.kind = ResourceKind::ScalarAddress;
const auto pointer = fixture.Emit(ValueOpcode::LoadAddressU32,
{root, Value(0u), Value(0u), Value(exec)}, fixture.AddMemory(address, 8));
const auto table = fixture.Address(pointer, Value(0u));
std::array<Value, 4> words;
for (uint32_t word = 0; word < words.size(); ++word) {
address.offset = 40 + word * 4;
words[word] = fixture.Emit(ValueOpcode::LoadAddressU32,
{table, Value(0u), Value(0u), Value(exec)}, fixture.AddMemory(address, 12));
}
scalar.offset = 0;
fixture.Emit(ValueOpcode::ReadConstBuffer, {fixture.Buffer(words), Value(0u)},
fixture.AddMemory(scalar, 16));
};
Load(optional);
if (shared) Load(done);
fixture.block = done;
MemoryInfo store;
store.kind = ResourceKind::Buffer;
fixture.Emit(ValueOpcode::StoreBufferU32,
{fixture.Buffer({Value(0x9000u), Value(0u), Value(4u), Value(0u)}),
Value(0u), Value(0u), Value(0u), Value(1u), Value(true)}, fixture.AddMemory(store, 20));
fixture.PlanAndTrack();
auto plan = ExtractResourcePlan(fixture.program);
Check(!exec || (!plan.srt_reads.empty() && !plan.control_flow[1].srt_reads.empty() &&
(!shared || !plan.control_flow[2].srt_reads.empty())),
"guarded shared-read fixture lost its flattened execution sites");
struct Memory { LinearTestMemory data; uint32_t null_reads = 0; } memory;
const auto Read = +[](void *data, uint64_t address, std::span<uint32_t> words) {
auto &memory = *static_cast<Memory *>(data);
if (address == 40) ++memory.null_reads;
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};
memory.data.words[0xa8 / 4] = 0x2000u;
memory.data.words[0xb0 / 4] = 4u;
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
if (!shared) {
Check(MaterializeResources(plan, runtime, snapshot, specialization) && memory.null_reads == 0 &&
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");
}
memory.data.words[0x40 / 4] = 0x1080u;
memory.data.watched_address = 0x10a8u;
memory.data.watched_reads = 0;
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
std::ranges::any_of(snapshot.buffers, [](const auto& descriptor) {
return descriptor.dwords[0] == 0x2000u;
}) &&
(!exec || (memory.data.watched_reads == 1 &&
std::ranges::find(snapshot.flattened_srt, 0x2000u) != snapshot.flattened_srt.end())),
"active or shared descriptor read was omitted after refreshing the cached plan");
if (!shared) {
memory.data.words[7] = 0;
memory.data.words[0x40 / 4] = 0;
Check(MaterializeResources(plan, runtime, snapshot, specialization) && memory.null_reads == 1 &&
std::ranges::all_of(snapshot.flattened_srt, [](auto word) { return word == 0; }),
"cached plan retained reads after the feature was disabled");
}
}
}
}
void TestConservativeBufferReachability() {
std::array<uint32_t, 8> user_data{
0x1000, 16u << 16u, 1, 0x4dfac,
@@ -2797,11 +2905,14 @@ void TestConservativeBufferReachability() {
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(user_data.begin() + 4, user_data.end(),
std::equal(expected.begin(), expected.end(),
snapshot.buffers[1].dwords.begin()),
"shared, loop-dependent, or writable-alias resource was pruned");
"shared or loop-dependent resource was pruned, or inactive scalar branch retained");
}
}
@@ -3187,6 +3298,7 @@ int main() {
Run("dynamic FLAT address", TestDynamicFlatAddressesUseDma);
Run("buffer swizzle specialization", TestBufferSwizzleSpecialization);
Run("conditional buffer materialization", TestConditionalBufferMaterialization);
Run("guarded scalar descriptor reads", TestGuardedScalarDescriptorReads);
Run("conservative buffer reachability", TestConservativeBufferReachability);
Run("conditional indirect image", TestConditionalIndirectImageMaterialization);
Run("shader info and bindings", TestShaderInfoAndBindingLayout);