shader: skip resources in proven empty unsigned loops

This commit is contained in:
nmzik committed 2026-10-17 10:38:14 +02:00
1 parent 56d03f16e9
commit c2173801d4
3 files changed
+73 -28

No files matched your search

@@ -1409,7 +1409,8 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
} }
if (op != ValueOpcode::LogicalAnd && op != ValueOpcode::LogicalOr && if (op != ValueOpcode::LogicalAnd && op != ValueOpcode::LogicalOr &&
op != ValueOpcode::LogicalNot && op != ValueOpcode::IEqual32 && op != ValueOpcode::LogicalNot && op != ValueOpcode::IEqual32 &&
op != ValueOpcode::INotEqual32 && op != ValueOpcode::ULessThanEqual32 && op != ValueOpcode::INotEqual32 && op != ValueOpcode::ULessThan32 &&
op != ValueOpcode::ULessThanEqual32 &&
op != ValueOpcode::SelectU1 && op != ValueOpcode::SelectU32) return finish(opaque()); op != ValueOpcode::SelectU1 && op != ValueOpcode::SelectU32) return finish(opaque());
auto& node = plan.value_storage.emplace_back(op); auto& node = plan.value_storage.emplace_back(op);
for (size_t index = 0; index < inst->NumArgs(); ++index) for (size_t index = 0; index < inst->NumArgs(); ++index)
@@ -1054,7 +1054,13 @@ bool SrtWalker::EvaluateInst(const Inst& inst, uint64_t& result) {
} }
return false; return false;
case ValueOpcode::ULessThan32: case ValueOpcode::ULessThan32:
if (binary()) { // No U32 is below zero, including an unknown loop-carried value.
if (!Arg(inst, 1, b)) return false;
if (static_cast<uint32_t>(b) == 0u) {
result = 0u;
return true;
}
if (Arg(inst, 0, a)) {
result = static_cast<uint32_t>(a) < static_cast<uint32_t>(b); result = static_cast<uint32_t>(a) < static_cast<uint32_t>(b);
return true; return true;
} }
+64 -26
View File
@@ -874,7 +874,7 @@ void TestFiniteImageRefreshReusesScalarReads() {
"finite image refresh grew reusable resource storage after warmup"); "finite image refresh grew reusable resource storage after warmup");
} }
void TestEmptyTraversalSkipsUnmappedDescriptor(bool independent_mask) { void TestEmptyTraversalSkipsUnmappedDescriptor(bool independent_mask, bool counted_loop = false) {
using namespace Libs::Graphics::ShaderRecompiler::IR; using namespace Libs::Graphics::ShaderRecompiler::IR;
using Libs::Graphics::ShaderRecompiler::CFG::TerminatorKind; using Libs::Graphics::ShaderRecompiler::CFG::TerminatorKind;
Program program; Program program;
@@ -898,54 +898,87 @@ void TestEmptyTraversalSkipsUnmappedDescriptor(bool independent_mask) {
block.AddBranch(&target); block.AddBranch(&target);
if (other) block.AddBranch(other); if (other) block.AddBranch(other);
}; };
const auto lane = emit(entry, ValueOpcode::GetBuiltin, Value lane, active, initial;
{Value(static_cast<uint32_t>(StageInputKind::LocalInvocationId)), Value(0u)}); if (!counted_loop) {
const auto active = emit(entry, ValueOpcode::INotEqual32, {lane, Value(0u)}); lane = emit(entry, ValueOpcode::GetBuiltin,
const auto initial_mask = independent_mask {Value(static_cast<uint32_t>(StageInputKind::LocalInvocationId)), Value(0u)});
? emit(entry, ValueOpcode::IEqual32, {lane, Value(0u)}) : active; active = emit(entry, ValueOpcode::INotEqual32, {lane, Value(0u)});
const auto initial = emit(entry, ValueOpcode::SelectU32, const auto initial_mask = independent_mask
{initial_mask, Value(UINT32_MAX), Value(0u)}); ? emit(entry, ValueOpcode::IEqual32, {lane, Value(0u)}) : active;
initial = emit(entry, ValueOpcode::SelectU32,
{initial_mask, Value(UINT32_MAX), Value(0u)});
}
program.memory_info.push_back({.kind = ResourceKind::ScalarAddress, .planning_only = true}); program.memory_info.push_back({.kind = ResourceKind::ScalarAddress, .planning_only = true});
const auto address = emit(entry, ValueOpcode::GetAddressResource, {Value(0x1000u), Value(0u)}); const auto address = emit(entry, ValueOpcode::GetAddressResource, {Value(0x1000u), Value(0u)});
const auto count = emit(entry, ValueOpcode::LoadAddressU32, const auto count = emit(entry, ValueOpcode::LoadAddressU32,
{address, Value(0u), Value(0u), Value(true)}); {address, Value(0u), Value(0u), Value(true)});
count.Instruction()->SetFlags(MemoryFlags{.index = 0}); count.Instruction()->SetFlags(MemoryFlags{.index = 0});
const auto rejected = emit(entry, ValueOpcode::ULessThanEqual32, {count, lane}); if (counted_loop) {
const auto accepted = emit(entry, ValueOpcode::LogicalNot, {rejected}); branch(entry, loop);
branch(entry, loop, emit(entry, ValueOpcode::LogicalAnd, {active, accepted}), &merge); } else {
const auto rejected = emit(entry, ValueOpcode::ULessThanEqual32, {count, lane});
const auto accepted = emit(entry, ValueOpcode::LogicalNot, {rejected});
branch(entry, loop, emit(entry, ValueOpcode::LogicalAnd, {active, accepted}), &merge);
}
// A nonempty traversal contains a varying loop-carried value. Its cycle must // A nonempty traversal contains a varying loop-carried value. Its cycle must
// remain unknown; only the proven empty traversal can select the bypass arm. // remain unknown; only the proven empty traversal can select the bypass arm.
auto &carried = loop.AppendNewInst(ValueOpcode::Phi); auto &carried = loop.AppendNewInst(ValueOpcode::Phi);
carried.SetFlags(Type::U32); carried.SetFlags(Type::U32);
const auto next = emit(body, ValueOpcode::IAdd32, {Value(&carried), Value(1u)}); const auto next = emit(body, ValueOpcode::IAdd32, {Value(&carried), Value(1u)});
carried.AddPhiOperand(&entry, lane); carried.AddPhiOperand(&entry, counted_loop ? Value(0u) : lane);
carried.AddPhiOperand(&body, next); carried.AddPhiOperand(&body, next);
branch(loop, body, emit(loop, ValueOpcode::INotEqual32, {Value(&carried), Value(UINT32_MAX)}), &merge); const auto condition = counted_loop
? emit(loop, ValueOpcode::ULessThan32, {Value(&carried), count})
: emit(loop, ValueOpcode::INotEqual32, {Value(&carried), Value(UINT32_MAX)});
branch(loop, counted_loop ? use : body, condition, &merge);
loop.terminator.loop_header = true; loop.terminator.loop_header = true;
loop.terminator.continue_block = &body; loop.terminator.continue_block = &body;
loop.terminator.merge_block = &merge; loop.terminator.merge_block = &merge;
branch(body, loop); branch(body, loop);
auto &selected = merge.AppendNewInst(ValueOpcode::Phi); if (counted_loop) {
selected.SetFlags(Type::U32); branch(merge, exit);
selected.AddPhiOperand(&entry, initial); branch(use, body);
selected.AddPhiOperand(&loop, Value(&carried)); } else {
const auto valid = emit(merge, ValueOpcode::INotEqual32, {Value(&selected), Value(UINT32_MAX)}); auto &selected = merge.AppendNewInst(ValueOpcode::Phi);
branch(merge, use, emit(merge, ValueOpcode::LogicalAnd, {active, valid}), &exit); selected.SetFlags(Type::U32);
branch(use, exit); selected.AddPhiOperand(&entry, initial);
selected.AddPhiOperand(&loop, Value(&carried));
const auto valid = emit(merge, ValueOpcode::INotEqual32, {Value(&selected), Value(UINT32_MAX)});
branch(merge, use, emit(merge, ValueOpcode::LogicalAnd, {active, valid}), &exit);
branch(use, exit);
}
DescriptorSource source; DescriptorSource source;
source.dwords = {Value(0x8000u), Value(1u << 16u), Value(16u), Value(3u << 28u)}; const auto descriptor_address = emit(use, ValueOpcode::GetAddressResource,
{Value(0x2000u), Value(0u)});
for (uint32_t i = 0; i < 4; ++i) {
const auto index = static_cast<uint32_t>(program.memory_info.size());
program.memory_info.push_back({.kind = ResourceKind::ScalarAddress, .planning_only = true});
source.dwords[i] = emit(use, ValueOpcode::LoadAddressU32,
{descriptor_address, Value(i * 4u), Value(0u), Value(true)});
source.dwords[i].Instruction()->SetFlags(MemoryFlags{.index = index});
}
source.dword_count = 4; source.dword_count = 4;
program.descriptor_sources.push_back(source); program.descriptor_sources.push_back(source);
program.info.buffers.push_back({.source = 0, .read = true}); program.info.buffers.push_back({.source = 0, .read = true});
const auto buffer_index = static_cast<uint32_t>(program.memory_info.size());
program.memory_info.push_back({.kind = ResourceKind::Buffer, .resource = 0}); program.memory_info.push_back({.kind = ResourceKind::Buffer, .resource = 0});
const auto buffer = emit(use, ValueOpcode::GetBufferResource, const auto buffer = emit(use, ValueOpcode::GetBufferResource,
{source.dwords[0], source.dwords[1], source.dwords[2], source.dwords[3]}); {source.dwords[0], source.dwords[1], source.dwords[2], source.dwords[3]});
emit(use, ValueOpcode::LoadBufferU32, {buffer, Value(0u), Value(0u), Value(0u), Value(true)}) emit(use, ValueOpcode::LoadBufferU32, {buffer, Value(0u), Value(0u), Value(0u), Value(true)})
.Instruction()->SetFlags(MemoryFlags{.index = 1}); .Instruction()->SetFlags(MemoryFlags{.index = buffer_index});
auto plan = ExtractResourcePlan(program); auto plan = ExtractResourcePlan(program);
struct Reads { uint32_t count = 0; bool clean = true; } reads; struct Reads { uint32_t count = 0; bool clean = true; uint32_t descriptors = 0; } reads;
const SrtRuntime runtime{ const SrtRuntime runtime{
.read_memory = +[](void *data, uint64_t address, std::span<uint32_t> words) {
constexpr std::array<uint32_t, 4> descriptor{0x8000u, 1u << 16u, 16u,
(3u << 28u) | 0x4dfacu};
if (address < 0x2000u || address >= 0x2010u || address % 4u || words.size() != 1)
return false;
++static_cast<Reads *>(data)->descriptors;
words[0] = descriptor[(address - 0x2000u) / 4u];
return true;
},
.userdata = &reads, .userdata = &reads,
.read_specialization_memory = +[](void *data, uint64_t address, std::span<uint32_t> words) { .read_specialization_memory = +[](void *data, uint64_t address, std::span<uint32_t> words) {
const auto &reads = *static_cast<Reads *>(data); const auto &reads = *static_cast<Reads *>(data);
@@ -957,9 +990,11 @@ void TestEmptyTraversalSkipsUnmappedDescriptor(bool independent_mask) {
ResourceSpecialization specialization; 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{0u, false}, Reads{0u, true}}) {
reads = state; reads = state;
uint32_t condition = 99; if (!counted_loop) {
Check(!SrtWalker(plan, runtime).Evaluate(plan.control_flow[3].condition, condition), uint32_t condition = 99;
"direct merge evaluation used missing or previous runtime reachability"); Check(!SrtWalker(plan, runtime).Evaluate(plan.control_flow[3].condition, condition),
"direct merge evaluation used missing or previous runtime reachability");
}
Check(MaterializeResources(plan, runtime, snapshot, specialization), Check(MaterializeResources(plan, runtime, snapshot, specialization),
"partial traversal proof rejected a valid resource snapshot"); "partial traversal proof rejected a valid resource snapshot");
const bool inactive = !independent_mask && reads.clean && reads.count == 0u; const bool inactive = !independent_mask && reads.clean && reads.count == 0u;
@@ -967,6 +1002,8 @@ void TestEmptyTraversalSkipsUnmappedDescriptor(bool independent_mask) {
"empty traversal kept its descriptor or an uncertain traversal lost it"); "empty traversal kept its descriptor or an uncertain traversal lost it");
Check(specialization.buffers[0].zero_stride_oob == inactive, Check(specialization.buffers[0].zero_stride_oob == inactive,
"inactive traversal did not expose dead vector reads to specialization"); "inactive traversal did not expose dead vector reads to specialization");
Check(reads.descriptors == (inactive ? 0u : 4u),
"inactive traversal read its descriptor or an uncertain traversal skipped it");
if (inactive) { if (inactive) {
Check(!snapshot.specialization_reads.empty(), "empty traversal lost its clean scalar read"); Check(!snapshot.specialization_reads.empty(), "empty traversal lost its clean scalar read");
for (const auto &read : snapshot.specialization_reads) for (const auto &read : snapshot.specialization_reads)
@@ -1331,6 +1368,7 @@ int main() {
TestFiniteImageRefreshReusesScalarReads(); TestFiniteImageRefreshReusesScalarReads();
for (const bool independent_mask : {false, true}) for (const bool independent_mask : {false, true})
TestEmptyTraversalSkipsUnmappedDescriptor(independent_mask); TestEmptyTraversalSkipsUnmappedDescriptor(independent_mask);
TestEmptyTraversalSkipsUnmappedDescriptor(false, true);
TestCyclicControlPredicatesRemainUnknown(); TestCyclicControlPredicatesRemainUnknown();
TestEmptyScalarBufferSpecialization(); TestEmptyScalarBufferSpecialization();
TestMixedSamplerVariantsShareRuntimeDescriptor(); TestMixedSamplerVariantsShareRuntimeDescriptor();