shader: capture only memory-dependent resource predicates

This commit is contained in:
nmzik
2026-10-09 15:04:01 +02:00
parent 488c0acae9
commit 3b1a94707a
4 changed files with 36 additions and 23 deletions
@@ -1007,10 +1007,15 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
// A proven uniform factor can decide a branch even when its other lanes are unknown. // A proven uniform factor can decide a branch even when its other lanes are unknown.
// Keep only that Boolean structure, never the varying shader dependency graph. // Keep only that Boolean structure, never the varying shader dependency graph.
Value unknown; Value unknown;
bool capture_control_reads = false;
std::function<Value(Value)> ClonePredicate = [&](Value value) -> Value { std::function<Value(Value)> ClonePredicate = [&](Value value) -> Value {
value = value.Resolve(); value = value.Resolve();
if (value.IsEmpty()) return {}; if (value.IsEmpty()) return {};
if (ValidateRuntimeValue(program, value, RuntimeValueType::Integer)) return Clone(value); if (ValidateRuntimeValue(program, value, RuntimeValueType::Integer)) {
capture_control_reads |=
!ValidateRuntimeValue(program, value, RuntimeValueType::ImmutableInteger);
return Clone(value);
}
const auto* inst = value.TryInstruction(); const auto* inst = value.TryInstruction();
if (inst == nullptr) return {}; if (inst == nullptr) return {};
const auto op = inst->GetOpcode(); const auto op = inst->GetOpcode();
@@ -1040,7 +1045,7 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
for (uint32_t i = 0; i < plan.uniform_fill.fill.words; ++i) { for (uint32_t i = 0; i < plan.uniform_fill.fill.words; ++i) {
plan.uniform_fill.values[i] = Clone(plan.uniform_fill.values[i]); plan.uniform_fill.values[i] = Clone(plan.uniform_fill.values[i]);
} }
plan.capture_specialization_reads |= capture_indirect_reads || !plan.control_flow.empty(); plan.capture_specialization_reads |= capture_indirect_reads || capture_control_reads;
if (plan.capture_specialization_reads) { if (plan.capture_specialization_reads) {
// Every former flat alias now targets its retained raw read. Mark those 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. // on the normalized graph, so discarded Phi/EXEC dependencies stay discarded.
@@ -966,7 +966,8 @@ bool SrtWalker::RefreshFlatBuffer(std::vector<uint32_t>& flat) {
} }
uint32_t condition = 0; uint32_t condition = 0;
auto& predicate = m_clean_evaluator != nullptr ? *m_clean_evaluator : *this; auto& predicate = m_clean_evaluator != nullptr ? *m_clean_evaluator : *this;
if (!block.condition.IsEmpty() && m_runtime.read_specialization_memory != nullptr && if (!block.condition.IsEmpty() &&
(m_runtime.read_specialization_memory != nullptr || !m_program.capture_specialization_reads) &&
predicate.Evaluate(block.condition, condition)) { predicate.Evaluate(block.condition, condition)) {
pending.push_back(block.successors[condition != 0u ? 0u : 1u]); pending.push_back(block.successors[condition != 0u ? 0u : 1u]);
} else { } else {
+23 -17
View File
@@ -435,7 +435,7 @@ void TestUnbasedFlatCacheHitMaterializes() {
"unbased FLAT plan produced unexpected descriptors"); "unbased FLAT plan produced unexpected descriptors");
} }
void TestWrittenDescriptorUsesStrictReaderOnce() { void TestWrittenDescriptorPredicateReads(bool memory_condition) {
using namespace Libs::Graphics::ShaderRecompiler::IR; using namespace Libs::Graphics::ShaderRecompiler::IR;
Program program; Program program;
program.stage = Libs::Graphics::ShaderType::Compute; program.stage = Libs::Graphics::ShaderType::Compute;
@@ -456,10 +456,10 @@ void TestWrittenDescriptorUsesStrictReaderOnce() {
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, .written = true}); program.info.buffers.push_back({.source = 0, .written = true});
// A host-evaluable branch captures resource reads and needs the writable // Only memory-derived host decisions need clean, disjoint descriptor reads.
// descriptor's clean provenance for the renderer's disjointness proof.
auto &condition = block.AppendNewInst(ValueOpcode::IEqual32, auto &condition = block.AppendNewInst(ValueOpcode::IEqual32,
{Value(&offset), Value(4u)}); {memory_condition ? Value(&read) : Value(&offset),
Value(memory_condition ? 0x8000u : 4u)});
auto &store_block = AddValueBlock(program); auto &store_block = AddValueBlock(program);
AddValueBlock(program); AddValueBlock(program);
program.blocks[0]->condition = Value(&condition); program.blocks[0]->condition = Value(&condition);
@@ -480,8 +480,8 @@ void TestWrittenDescriptorUsesStrictReaderOnce() {
{Value(&output), Value(0u), Value(0u), Value(0u), Value(1u), Value(true)}) {Value(&output), Value(0u), Value(0u), Value(0u), Value(1u), Value(true)})
.SetFlags(MemoryFlags{.index = 1}); .SetFlags(MemoryFlags{.index = 1});
auto plan = ExtractResourcePlan(program); auto plan = ExtractResourcePlan(program);
Check(plan.capture_specialization_reads, Check(plan.capture_specialization_reads == memory_condition && !plan.control_flow.empty(),
"conditional writable descriptor lost its alias proof"); "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 ordinary = 0; uint32_t strict = 0; bool clean = false; } reads;
const std::array<uint32_t, 1> user_data{4u}; const std::array<uint32_t, 1> user_data{4u};
const SrtRuntime runtime{ const SrtRuntime runtime{
@@ -501,16 +501,21 @@ void TestWrittenDescriptorUsesStrictReaderOnce() {
}}; }};
ResourceSnapshot snapshot; ResourceSnapshot snapshot;
ResourceSpecialization specialization; ResourceSpecialization specialization;
Check(!MaterializeResources(plan, runtime, snapshot, specialization) && for (const bool clean : {false, true}) {
reads.ordinary == 0 && reads.strict == 1, reads = {.clean = clean};
"GPU-dirty dynamic writable descriptor bypassed strict provenance"); // A failed predicate explores both edges and the writable descriptor retries its clean read.
reads.clean = true; const bool expected = !memory_condition || clean;
reads.strict = 0; Check(MaterializeResources(plan, runtime, snapshot, specialization) == expected &&
Check(MaterializeResources(plan, runtime, snapshot, specialization) && reads.ordinary == (memory_condition ? 0u : 1u) &&
reads.ordinary == 0 && reads.strict == 1 && snapshot.buffers[0].dwords[0] == 0x8000u && reads.strict == (memory_condition ? (clean ? 1u : 2u) : 0u),
snapshot.specialization_reads == "writable descriptor changed reader policy or bypassed a dirty memory predicate");
std::vector<std::pair<uint64_t, uint64_t>>{{0x1004u, 4u}}, if (!expected) continue;
"writable descriptor was evaluated twice or scalar EXEC suppressed its read"); Check(snapshot.buffers[0].dwords[0] == 0x8000u &&
snapshot.specialization_reads == (memory_condition
? std::vector<std::pair<uint64_t, uint64_t>>{{0x1004u, 4u}}
: std::vector<std::pair<uint64_t, uint64_t>>{}),
"writable descriptor lost its address or captured an immutable predicate");
}
} }
void TestFailedMaterializationRejectsStage() { void TestFailedMaterializationRejectsStage() {
@@ -671,7 +676,8 @@ int main() {
TestUniformVectorDescriptorRead(); TestUniformVectorDescriptorRead();
TestExactReciprocalDescriptorArithmetic(); TestExactReciprocalDescriptorArithmetic();
TestUnbasedFlatCacheHitMaterializes(); TestUnbasedFlatCacheHitMaterializes();
TestWrittenDescriptorUsesStrictReaderOnce(); for (const bool memory_condition : {false, true})
TestWrittenDescriptorPredicateReads(memory_condition);
TestFailedMaterializationRejectsStage(); TestFailedMaterializationRejectsStage();
TestFiniteImageRefreshReusesScalarReads(); TestFiniteImageRefreshReusesScalarReads();
TestMixedSamplerVariantsShareRuntimeDescriptor(); TestMixedSamplerVariantsShareRuntimeDescriptor();
+4 -3
View File
@@ -13779,7 +13779,7 @@ void TestUniformSelectedDescriptorLoadAddress() {
auto translated = ShaderRecompiler::TranslateProgram(shader, options); auto translated = ShaderRecompiler::TranslateProgram(shader, options);
auto plan = ExtractResourcePlan(translated.program); auto plan = ExtractResourcePlan(translated.program);
Check(plan.srt_reads.size() == 4u && plan.info.buffers.size() == 1u && Check(plan.srt_reads.size() == 4u && plan.info.buffers.size() == 1u &&
plan.info.buffers[0].written, plan.info.buffers[0].written && !plan.capture_specialization_reads,
"conditional scalar address duplicated or detached descriptor reads"); "conditional scalar address duplicated or detached descriptor reads");
const auto read = +[](void *data, uint64_t address, std::span<uint32_t> words) { const auto read = +[](void *data, uint64_t address, std::span<uint32_t> words) {
const auto flag = *static_cast<uint32_t *>(data); const auto flag = *static_cast<uint32_t *>(data);
@@ -13792,13 +13792,14 @@ void TestUniformSelectedDescriptorLoadAddress() {
return true; return true;
}; };
const SrtRuntime runtime{.user_data = user_data, .read_memory = read, const SrtRuntime runtime{.user_data = user_data, .read_memory = read,
.userdata = &user_data[0], .read_specialization_memory = read}; .userdata = &user_data[0]};
ResourceSnapshot snapshot; ResourceSnapshot snapshot;
ResourceSpecialization specialization; ResourceSpecialization specialization;
for (const uint32_t flag : {0u, 1u, 0u}) { for (const uint32_t flag : {0u, 1u, 0u}) {
user_data[0] = flag; user_data[0] = flag;
Check(MaterializeResources(plan, runtime, snapshot, specialization) && Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
snapshot.buffers[0].dwords[0] == 0x3000u + flag * 0x1000u, snapshot.buffers[0].dwords[0] == 0x3000u + flag * 0x1000u &&
snapshot.specialization_reads.empty(),
"conditional descriptor load read the untaken pointer"); "conditional descriptor load read the untaken pointer");
} }
} }