From 59902d1fd87d9cb2849d1c99c3fd462858fd3951 Mon Sep 17 00:00:00 2001 From: nmzik Date: Sat, 5 Sep 2026 09:38:50 +0400 Subject: [PATCH] shader: skip resources in untaken uniform branches Co-authored-by: GodsWill Co-authored-by: Techx3 <56268269+Techx3@users.noreply.github.com> --- src/graphics/shader/recompiler/ir/ShaderIR.h | 12 +- .../ir/passes/ResourceMaterialization.cpp | 115 ++++++++++- .../shader/recompiler/ir/passes/SrtWalker.cpp | 61 ++++-- .../shader/recompiler/ir/passes/SrtWalker.h | 7 +- tests/ResourceTrackingTests.cpp | 189 +++++++++++++++++- 5 files changed, 362 insertions(+), 22 deletions(-) diff --git a/src/graphics/shader/recompiler/ir/ShaderIR.h b/src/graphics/shader/recompiler/ir/ShaderIR.h index b98090cf2..fce7374d5 100644 --- a/src/graphics/shader/recompiler/ir/ShaderIR.h +++ b/src/graphics/shader/recompiler/ir/ShaderIR.h @@ -470,6 +470,13 @@ struct SrtRead { bool operator==(const SrtRead& other) const = default; }; +struct ResourceBlock { + // Conditional successors are ordered true, false; an empty condition follows every edge. + Value condition; + std::vector successors; + std::vector sources; +}; + // Stable shader metadata consumed by the renderer after native IR has been discarded. struct CompiledShaderInfo { ShaderType stage = ShaderType::Unknown; @@ -483,8 +490,8 @@ struct CompiledShaderInfo { BindingLayout bindings; }; -// Immutable runtime resource analysis retained by the shader cache. It owns only the native -// value graph reachable from descriptors/SRT reads, rather than the translated shader CFG. +// Immutable runtime resource analysis retained by the shader cache. It owns the descriptor/SRT +// and uniform condition values, plus resource-use edges, without retaining translated blocks. struct ResourcePlan { ResourcePlan() = default; ~ResourcePlan(); @@ -501,6 +508,7 @@ struct ResourcePlan { std::list value_storage; std::vector memory_info; std::vector descriptor_sources; + std::vector control_flow; std::vector materialization_sources; std::vector srt_reads; std::vector clean_flat_slots; diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp index ba2df4156..8f7e9f7a3 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp @@ -293,8 +293,9 @@ static bool MaterializeSnapshot(const ResourcePlan& program, const SrtRuntime& r } std::vector values; std::vector flattened_srt; + std::vector active_sources; if (!EvaluateRuntimeSources(program, program.materialization_sources, runtime, values, - flattened_srt, program.clean_flat_slots)) { + flattened_srt, program.clean_flat_slots, active_sources)) { return false; } @@ -308,6 +309,10 @@ static bool MaterializeSnapshot(const ResourcePlan& program, const SrtRuntime& r const auto& image = program.info.images[image_index]; const auto* source = Source(program, image.source); if (source != nullptr && source->indirect_image.has_value()) { + if (!active_sources[image.source]) { + next.images[image_index].dword_count = 8u; + continue; + } const std::array requests {source->indirect_image->material_source, source->indirect_image->heap_source}; SrtRuntime clean_runtime = runtime; @@ -604,6 +609,110 @@ bool BuildSamplerPlan(const ShaderInfo& base, const Images& images, SamplerPlan& return true; } +static bool IntegerCondition(const ResourcePlan& program, Value condition) { + if (!ValidateRuntimeValue(program, condition)) { + return false; + } + std::vector pending {condition}; + std::unordered_set visited; + while (!pending.empty()) { + const auto value = pending.back().Resolve(); + pending.pop_back(); + // Host floating-point evaluation does not model the shader's rounding/denormal modes. + if (TypesOverlap(value.GetType(), Type::F16 | Type::F32 | Type::F32x2)) { + return false; + } + const auto* inst = value.TryInstruction(); + if (inst != nullptr && visited.insert(inst).second) { + if (inst->GetOpcode() == ValueOpcode::ReadConst) { + const auto& read = program.srt_reads[inst->Arg(1).Resolve().U32()]; + if (!ValidateRuntimeValue(program, read.value)) { + return false; + } + pending.push_back(read.value); + } + for (size_t i = 0; i < inst->NumArgs(); i++) { + pending.push_back(inst->Arg(i)); + } + } + } + return true; +} + +static std::vector ResourceControlFlow(const Program& program) { + if (program.blocks.size() != program.block_info.size()) { + return {}; + } + std::unordered_map indices; + for (uint32_t i = 0; i < program.block_info.size(); i++) { + if (!indices.emplace(program.block_info[i].id, i).second) { + return {}; + } + } + std::vector blocks(program.blocks.size()); + for (uint32_t i = 0; i < blocks.size(); i++) { + auto& block = blocks[i]; + const auto& info = program.block_info[i]; + const auto& terminator = info.terminator; + std::vector successors; + switch (terminator.kind) { + case CFG::TerminatorKind::Branch: successors.push_back(terminator.true_block); break; + case CFG::TerminatorKind::ConditionalBranch: + successors = {terminator.true_block, terminator.false_block}; + if (IntegerCondition(program, info.condition)) { + block.condition = info.condition; + } + break; + case CFG::TerminatorKind::IndirectBranch: + successors = terminator.indirect_targets; + break; + case CFG::TerminatorKind::Return: break; + default: return {}; + } + for (const auto successor: successors) { + const auto found = indices.find(successor); + if (found == indices.end()) { + return {}; + } + block.successors.push_back(found->second); + } + for (const auto& inst: *program.blocks[i]) { + const auto op = inst.GetOpcode(); + const auto buffer = BufferAccessOf(op); + const auto image = ImageOpcodeInfoOf(op); + // Any shader write may alias a scalar predicate read, including on a later loop visit. + if (buffer == BufferAccess::Write || buffer == BufferAccess::Atomic || + image.access == ImageAccess::Write || image.access == ImageAccess::Atomic || + AddressOpcodeInfoOf(op).access == AddressAccess::Write) { + return {}; + } + if (buffer == BufferAccess::None && image.access == ImageAccess::None) { + continue; + } + const auto& memory = program.memory_info.at(inst.Flags().index); + if (memory.planning_only) { + continue; + } + if (buffer != BufferAccess::None) { + block.sources.push_back(program.info.buffers.at(memory.resource).source); + } else { + block.sources.push_back(program.info.images.at(memory.resource).source); + if (image.needs_sampler) { + block.sources.push_back(program.info.samplers.at(memory.sampler).source); + } + } + } + std::ranges::sort(block.sources); + block.sources.erase(std::unique(block.sources.begin(), block.sources.end()), + block.sources.end()); + } + if (std::ranges::none_of( + blocks, [](const ResourceBlock& block) { return !block.condition.IsEmpty(); })) { + return {}; + } + return blocks; +} + ResourcePlan ExtractResourcePlan(const Program& program) { ResourcePlan plan; plan.stage = program.stage; @@ -659,6 +768,10 @@ ResourcePlan ExtractResourcePlan(const Program& program) { for (const auto& read: program.srt_reads) { plan.srt_reads.push_back({Clone(read.value), read.flat_offset}); } + plan.control_flow = ResourceControlFlow(program); + for (auto& block: plan.control_flow) { + block.condition = Clone(block.condition); + } plan.materialization_sources.reserve(plan.info.buffers.size() + plan.info.images.size() + plan.info.samplers.size()); for (const auto& buffer: plan.info.buffers) { diff --git a/src/graphics/shader/recompiler/ir/passes/SrtWalker.cpp b/src/graphics/shader/recompiler/ir/passes/SrtWalker.cpp index 47f2b5a25..3685224a3 100644 --- a/src/graphics/shader/recompiler/ir/passes/SrtWalker.cpp +++ b/src/graphics/shader/recompiler/ir/passes/SrtWalker.cpp @@ -472,10 +472,11 @@ private: } m_visiting.push_back(inst); uint64_t out = 0; - if (!EvaluateInst(*inst, out)) { + const bool evaluated = EvaluateInst(*inst, out); + m_visiting.pop_back(); + if (!evaluated) { return false; } - m_visiting.pop_back(); m_cache.emplace(inst, out); result = out; return true; @@ -938,7 +939,8 @@ const DescriptorSource* Source(const ResourcePlan& program, uint32_t source) { bool EvaluateRuntimeSourcesImpl(const ResourcePlan& program, std::span sources, const SrtRuntime& runtime, std::vector& results, std::vector& flat, bool evaluate_flat, - std::span clean_flat_slots) { + std::span clean_flat_slots, + std::vector& active_sources) { if (!program.srt_plan_complete) { return false; } @@ -948,8 +950,38 @@ bool EvaluateRuntimeSourcesImpl(const ResourcePlan& program, std::span active(program.descriptor_sources.size(), 1u); + if (evaluate_flat && !program.control_flow.empty()) { + for (const auto& block: program.control_flow) { + for (const auto source: block.sources) { + active.at(source) = 0u; + } + } + std::vector visited(program.control_flow.size()); + std::vector pending {0}; + while (!pending.empty()) { + const auto index = pending.back(); + pending.pop_back(); + if (visited.at(index)) { + continue; + } + visited[index] = 1u; + const auto& block = program.control_flow[index]; + for (const auto source: block.sources) { + active[source] = 1u; + } + uint32_t condition = 0; + // A missing clean reader must never fall through to the evaluator's raw-memory path. + if (!block.condition.IsEmpty() && runtime.read_specialization_memory != nullptr && + clean_evaluator.Evaluate(block.condition, condition)) { + pending.push_back(block.successors[condition != 0u ? 0u : 1u]); + } else { + pending.insert(pending.end(), block.successors.begin(), block.successors.end()); + } + } + } std::vector evaluated; evaluated.reserve(sources.size()); for (const auto source_index: sources) { @@ -959,7 +991,7 @@ bool EvaluateRuntimeSourcesImpl(const ResourcePlan& program, std::spandword_count; - for (uint32_t index = 0; index < source->dword_count; index++) { + for (uint32_t index = 0; active[source_index] && index < source->dword_count; index++) { if (!evaluator.Evaluate(source->dwords[index], value.dwords[index])) { return false; } @@ -980,6 +1012,7 @@ bool EvaluateRuntimeSourcesImpl(const ResourcePlan& program, std::span sources, const SrtRuntime& runtime, std::vector& results) { std::vector ignored; - return EvaluateRuntimeSourcesImpl(program, sources, runtime, results, ignored, false, {}); + std::vector active; + return EvaluateRuntimeSourcesImpl(program, sources, runtime, results, ignored, false, {}, + active); } bool EvaluateRuntimeSources(const ResourcePlan& program, std::span sources, const SrtRuntime& runtime, std::vector& results, - std::vector& flat, - std::span clean_flat_slots) { + std::vector& flat, std::span clean_flat_slots, + std::vector& active_sources) { return EvaluateRuntimeSourcesImpl(program, sources, runtime, results, flat, true, - clean_flat_slots); + clean_flat_slots, active_sources); } -bool WalkSrt(const ResourcePlan& program, const SrtRuntime& runtime, - std::vector& flat) { +bool WalkSrt(const ResourcePlan& program, const SrtRuntime& runtime, std::vector& flat) { std::vector ignored; - return EvaluateRuntimeSources(program, {}, runtime, ignored, flat, {}); + std::vector active; + return EvaluateRuntimeSources(program, {}, runtime, ignored, flat, {}, active); } } // namespace Libs::Graphics::ShaderRecompiler::IR diff --git a/src/graphics/shader/recompiler/ir/passes/SrtWalker.h b/src/graphics/shader/recompiler/ir/passes/SrtWalker.h index 839a69fb3..c7a9163b5 100644 --- a/src/graphics/shader/recompiler/ir/passes/SrtWalker.h +++ b/src/graphics/shader/recompiler/ir/passes/SrtWalker.h @@ -32,11 +32,12 @@ bool EvaluateDescriptorSource(const ResourcePlan& program, uint32_t source, bool EvaluateDescriptorSources(const ResourcePlan& program, std::span sources, const SrtRuntime& runtime, std::vector& results); -// Evaluates descriptor sources and the flattened immediate SRT with one memoized scalar walk. -// On failure neither destination is changed. +// Evaluates potentially reachable descriptor sources and the flattened immediate SRT with one +// memoized scalar walk. Inactive descriptors are zero; on failure no destination is changed. bool EvaluateRuntimeSources(const ResourcePlan& program, std::span sources, const SrtRuntime& runtime, std::vector& results, - std::vector& flat, std::span clean_flat_slots); + std::vector& flat, std::span clean_flat_slots, + std::vector& active_sources); bool WalkSrt(const ResourcePlan& program, const SrtRuntime& runtime, std::vector& flat); diff --git a/tests/ResourceTrackingTests.cpp b/tests/ResourceTrackingTests.cpp index 19bb6688b..0a8e39c2e 100644 --- a/tests/ResourceTrackingTests.cpp +++ b/tests/ResourceTrackingTests.cpp @@ -900,9 +900,10 @@ void TestSrtFlatteningAndRuntimeMemoization() { .userdata = &memory}; std::vector descriptors; std::vector flat; + std::vector active_sources; const uint32_t request = fixture.program.info.buffers[0].source; Check(EvaluateRuntimeSources(fixture.program, std::span{&request, 1}, runtime, - descriptors, flat, {}), + descriptors, flat, {}, active_sources), "typed runtime source evaluation failed"); Check(descriptors.size() == 1 && descriptors[0].dwords[0] == 0xdeadbeefu && flat == std::vector{0xdeadbeefu} && memory.reads == 1, @@ -912,10 +913,12 @@ void TestSrtFlatteningAndRuntimeMemoization() { memory.fail_after = 0; descriptors = {{{1u}, 1u}}; flat = {2u}; + active_sources = {3u}; Check(!EvaluateRuntimeSources(fixture.program, std::span{&request, 1}, - runtime, descriptors, flat, {}) && + runtime, descriptors, flat, {}, active_sources) && descriptors == std::vector{{{1u}, 1u}} && - flat == std::vector{2u}, + flat == std::vector{2u} && + active_sources == std::vector{3u}, "runtime evaluation failure was not transactional"); ShaderComputeInputInfo compute{}; @@ -1188,6 +1191,183 @@ void TestBufferSwizzleSpecialization() { "buffer swizzle change did not select a new specialization key"); } +enum class ConditionalBufferUse { Optional, Shared, Loop, Writable }; + +ResourcePlan ConditionalBufferPlan(ConditionalBufferUse use) { + namespace CFG = Libs::Graphics::ShaderRecompiler::CFG; + Fixture fixture; + auto *entry = fixture.block; + auto *optional = fixture.AddBlock(); + auto *done = fixture.AddBlock(); + auto *condition_block = entry; + uint32_t condition_index = 0; + fixture.program.block_info[0].id = 11; + fixture.program.block_info[1].id = 27; + fixture.program.block_info[2].id = 42; + if (use == ConditionalBufferUse::Loop) { + condition_block = fixture.AddBlock(); + condition_index = 3; + fixture.program.block_info[3].id = 55; + fixture.program.block_info[0].terminator = { + .kind = CFG::TerminatorKind::Branch, .true_block = 55}; + entry->AddBranch(condition_block); + } + condition_block->AddBranch(optional); + condition_block->AddBranch(done); + optional->AddBranch(use == ConditionalBufferUse::Loop ? condition_block : done); + fixture.program.block_info[condition_index].terminator = { + .kind = CFG::TerminatorKind::ConditionalBranch, + .true_block = 27, .false_block = 42}; + fixture.program.block_info[1].terminator = { + .kind = CFG::TerminatorKind::Branch, + .true_block = use == ConditionalBufferUse::Loop ? 55u : 42u}; + + const auto control = fixture.Buffer( + {fixture.UserData(0), fixture.UserData(1), fixture.UserData(2), + fixture.UserData(3)}, 4); + MemoryInfo scalar; + scalar.kind = ResourceKind::ScalarBuffer; + auto flag = fixture.Emit(ValueOpcode::ReadConstBuffer, + {control, Value(0u)}, fixture.AddMemory(scalar, 4)); + if (use == ConditionalBufferUse::Loop) { + auto &phi = condition_block->AppendNewInst(ValueOpcode::Phi, {}, + static_cast(Type::U32)); + phi.AddPhiOperand(entry, flag); + phi.AddPhiOperand(optional, Value(1u)); + flag = Value(&phi); + } + fixture.program.block_info[condition_index].condition = + fixture.Emit(ValueOpcode::INotEqual32, {flag, Value(0u)}, 0, condition_block); + + const auto payload = fixture.Buffer( + {fixture.UserData(4), fixture.UserData(5), fixture.UserData(6), + fixture.UserData(7)}, 8); + MemoryInfo vector; + vector.kind = ResourceKind::Buffer; + const auto load = [&](Block *block) { + fixture.Emit(ValueOpcode::LoadBufferU32, + {payload, Value(0u), Value(0u), Value(0u), Value(true)}, + fixture.AddMemory(vector, 8), block); + }; + load(optional); + if (use == ConditionalBufferUse::Shared) { + load(done); + } + if (use == ConditionalBufferUse::Writable) { + fixture.Emit(ValueOpcode::StoreBufferU32, + {control, Value(0u), Value(0u), Value(0u), Value(1u), + Value(true)}, fixture.AddMemory(vector, 12)); + } + fixture.PlanAndTrack(); + return ExtractResourcePlan(fixture.program); +} + +void TestConditionalBufferMaterialization() { + auto plan = ConditionalBufferPlan(ConditionalBufferUse::Optional); + // GTA III leaves packet words in s[12:15] when its scalar control word is zero. + std::array user_data{ + 0x1000, 16u << 16u, 1, 0x4dfac, + 0xc0107600, 0x8c, 0x97730000, 0x100020}; + TestMemory memory; + SrtRuntime runtime{.user_data = user_data, .userdata = &memory, + .read_specialization_memory = ReadTestMemory}; + ResourceSnapshot snapshot; + ResourceSpecialization specialization; + Check(MaterializeResources(plan, runtime, snapshot, specialization) && + snapshot.buffers.size() == 2 && + snapshot.buffers[1].dword_count == 4 && + snapshot.buffers[1].dwords == std::array{}, + "untaken scalar branch materialized stale buffer words"); + Check(snapshot.user_data == std::vector(user_data.begin(), user_data.end()), + "resource reachability changed native shader user data"); + + runtime.user_data = std::span(user_data).first(4); + Check(MaterializeResources(plan, runtime, snapshot, specialization), + "untaken branch evaluated its unavailable descriptor"); + const auto prior = snapshot; + memory.words[0] = 1; + Check(!MaterializeResources(plan, runtime, snapshot, specialization) && + SameResourceSnapshot(snapshot, prior), + "taken branch accepted an unavailable descriptor or changed the snapshot"); + + runtime.user_data = user_data; + const auto CheckActive = [&] { + Check(MaterializeResources(plan, runtime, snapshot, specialization) && + snapshot.buffers.size() == 2 && + std::equal(user_data.begin() + 4, user_data.end(), + snapshot.buffers[1].dwords.begin()), + "potentially executed buffer descriptor was discarded"); + }; + CheckActive(); + memory.words[0] = 0; + memory.fail_after = memory.reads; + CheckActive(); + runtime.read_specialization_memory = nullptr; + CheckActive(); +} + +void TestConservativeBufferReachability() { + std::array user_data{ + 0x1000, 16u << 16u, 1, 0x4dfac, + 0x2000, 16u << 16u, 1, 0x4dfac}; + TestMemory memory; + const SrtRuntime runtime{.user_data = user_data, .userdata = &memory, + .read_specialization_memory = ReadTestMemory}; + for (const auto use : {ConditionalBufferUse::Shared, ConditionalBufferUse::Loop, + ConditionalBufferUse::Writable}) { + auto plan = ConditionalBufferPlan(use); + ResourceSnapshot snapshot; + ResourceSpecialization specialization; + Check(MaterializeResources(plan, runtime, snapshot, specialization) && + snapshot.buffers.size() == 2 && + std::equal(user_data.begin() + 4, user_data.end(), + snapshot.buffers[1].dwords.begin()), + "shared, loop-dependent, or writable-alias resource was pruned"); + } +} + +void TestConditionalIndirectImageMaterialization() { + namespace CFG = Libs::Graphics::ShaderRecompiler::CFG; + auto fixture = MakeIndirectImageFixture(false); + auto *body = fixture->block; + auto *entry = fixture->AddBlock(); + auto *done = fixture->AddBlock(); + entry->AddBranch(body); + entry->AddBranch(done); + body->AddBranch(done); + const auto flag = fixture->Emit(ValueOpcode::GetUserData, + {Value(static_cast(8))}, 0, entry); + fixture->program.block_info[1].condition = fixture->Emit( + ValueOpcode::INotEqual32, {flag, Value(0u)}, 0, entry); + fixture->program.block_info[1].terminator = { + .kind = CFG::TerminatorKind::ConditionalBranch, + .true_block = 0, .false_block = 2}; + fixture->program.block_info[0].terminator = { + .kind = CFG::TerminatorKind::Branch, .true_block = 2}; + std::swap(fixture->program.blocks[0], fixture->program.blocks[1]); + std::swap(fixture->program.block_info[0], fixture->program.block_info[1]); + fixture->PlanAndTrack(); + auto plan = ExtractResourcePlan(fixture->program); + std::array user_data{ + 0x1000, 224u << 16u, 2, 0, 0x2000, 16u << 16u, 4, 0, 0}; + uint32_t reads = 0; + const SrtRuntime runtime{ + .user_data = user_data, .userdata = &reads, + .read_specialization_memory = [](void *data, uint64_t, uint32_t *) { + ++*static_cast(data); + return false; + }}; + ResourceSnapshot snapshot; + ResourceSpecialization specialization; + Check(MaterializeResources(plan, runtime, snapshot, specialization) && + reads == 0 && snapshot.images.size() == 1 && + snapshot.images[0].dwords == std::array{}, + "untaken indirect image branch probed its descriptor table"); + user_data[8] = 1; + Check(!MaterializeResources(plan, runtime, snapshot, specialization) && reads != 0, + "taken indirect image branch did not require its descriptor table"); +} + void TestShaderInfoAndBindingLayout() { Fixture fixture; const auto handle = fixture.Buffer( @@ -1484,6 +1664,9 @@ int main() { Run("DMA address materialization", TestDmaAddressMaterialization); Run("dynamic FLAT address", TestDynamicFlatAddressesUseDma); Run("buffer swizzle specialization", TestBufferSwizzleSpecialization); + Run("conditional buffer materialization", TestConditionalBufferMaterialization); + Run("conservative buffer reachability", TestConservativeBufferReachability); + Run("conditional indirect image", TestConditionalIndirectImageMaterialization); Run("shader info and bindings", TestShaderInfoAndBindingLayout); Run("image binding ABI", TestImageBindingAbi); Run("graphics push constants", TestGraphicsPushConstantLayout);