From 745cdb5bf746e604601033dfb077bbaa42a3fe9a Mon Sep 17 00:00:00 2001 From: ThatsLeo Date: Wed, 14 Oct 2026 22:48:23 +0200 Subject: [PATCH] perf(shader): judge first-lane and constant reads by what they read MayEvaluate took ReadFirstLane and ReadConst as evaluable without looking at their sources, so plans kept conditions behind per-lane values. --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 22 +++++++++- .../Optimization/SrtWalker/SrtEvaluator.hpp | 2 +- .../src/SrtWalker/SrtEvaluator.cpp | 41 +++++++++++++++---- docs/dev/ARCHITECTURE.md | 2 +- 4 files changed, 56 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 51dee75d38..7b994fd6f4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -561,9 +561,19 @@ void verifyMayEvaluate() { counter.AddArgument(&next); auto& latch = make(IrOpcode::ULessThan32, IrType::Bool, {&next, &constant(4)}); auto& sum = make(IrOpcode::CompositeExtractU32x2, IrType::U32, {&make(IrOpcode::IAddCarry32, IrType::U32x2, {&input, &constant(1)}), &constant(0)}); + auto& firstInput = make(IrOpcode::ReadFirstLane, IrType::U32, {&input, &varying}); + auto& firstLane = make(IrOpcode::ReadFirstLane, IrType::U32, {&lane, &varying}); + auto& firstActive = make(IrOpcode::ReadFirstLane, IrType::U32, {&make(IrOpcode::SelectU32, IrType::U32, {&varying, &input, &lane}), &varying}); + auto& firstInactive = make(IrOpcode::ReadFirstLane, IrType::U32, {&make(IrOpcode::SelectU32, IrType::U32, {&varying, &lane, &input}), &varying}); + plan.srtReads = {{&input, 0u}, {&lane, 4u}, {nullptr, 8u}}; + auto& constInput = make(IrOpcode::ReadConst, IrType::U32, {&constant(0), &constant(0)}); + auto& constLane = make(IrOpcode::ReadConst, IrType::U32, {&constant(0), &constant(1)}); + auto& constOutside = make(IrOpcode::ReadConst, IrType::U32, {&constant(0), &constant(3)}); + auto& constItself = make(IrOpcode::ReadConst, IrType::U32, {&constant(0), &constant(2)}); + plan.srtReads[2].value = &constItself; std::array data{4}; SrtRuntime runtime{data}; - for (const auto& [value, expected, name] : std::array, 11>{{ + for (const auto& [value, expected, name] : std::array, 19>{{ {&constant(3), true, "a constant"}, {&input, true, "user data"}, {&uniform, true, "a comparison of user data"}, @@ -575,13 +585,21 @@ void verifyMayEvaluate() { {&divergent, false, "a phi whose operands differ"}, {&latch, false, "a scalar loop counter's latch condition"}, {&sum, true, "the low word of a carry add of user data"}, + {&firstInput, true, "a first-lane read of user data"}, + {&firstLane, false, "a first-lane read of a vector register"}, + {&firstActive, true, "a first-lane read of a select whose active arm is user data"}, + {&firstInactive, false, "a first-lane read of a select whose active arm is a vector register"}, + {&constInput, true, "a constant read whose slot holds user data"}, + {&constLane, false, "a constant read whose slot holds a vector register"}, + {&constOutside, false, "a constant read past the last slot"}, + {&constItself, false, "a constant read whose slot reads itself"}, }}) { require(Detail::Evaluator::MayEvaluate(plan, value) == expected, (std::string("MayEvaluate misjudged ") + name).c_str()); Detail::Evaluator evaluator(plan, runtime); std::uint32_t result = 0; require(evaluator.Evaluate(Detail::IrNode(value), result) == expected, (std::string("MayEvaluate disagreed with the evaluator on ") + name).c_str()); } - const std::array takenAsEvaluable{IrOpcode::Identity, IrOpcode::Phi, IrOpcode::Void, IrOpcode::GetUserData, IrOpcode::GetShaderBase, IrOpcode::ReadFirstLane, IrOpcode::ReadConst, IrOpcode::LoadAddressU32, IrOpcode::ReadConstBuffer}; + const std::array takenAsEvaluable{IrOpcode::Identity, IrOpcode::Phi, IrOpcode::Void, IrOpcode::GetUserData, IrOpcode::GetShaderBase, IrOpcode::LoadAddressU32, IrOpcode::ReadConstBuffer}; for (std::uint16_t raw = 0; raw < static_cast(IrOpcode::Count); ++raw) { const auto opcode = static_cast(raw); if (std::ranges::find(takenAsEvaluable, opcode) != takenAsEvaluable.end()) continue; diff --git a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtEvaluator.hpp b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtEvaluator.hpp index 2139ae7a61..7a979b4719 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtEvaluator.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtEvaluator.hpp @@ -171,7 +171,7 @@ public: BasicEvaluator(const BasicEvaluator&) = delete; BasicEvaluator& operator=(const BasicEvaluator&) = delete; - static bool MayEvaluate(const IrResourcePlan& program, IrValue* value); + static bool MayEvaluate(const IrResourcePlan& program, IrValue* value, const IrValue* activeMask = nullptr); bool Evaluate(TNode value, std::uint32_t& result); bool EvaluateWide(TNode value, std::uint64_t& result); void ReportInaccessibleReads(InaccessibleRead* sink) { _inaccessible = sink; } diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp index d9a5fbccf7..d99fbabc20 100644 --- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -327,9 +328,13 @@ std::optional evaluableAlone(const IrResourcePlan& program, const IrValue& } switch (value.Opcode()) { case IrOpcode::GetUserData: - case IrOpcode::GetShaderBase: - case IrOpcode::ReadFirstLane: - case IrOpcode::ReadConst: return true; + case IrOpcode::GetShaderBase: return true; + case IrOpcode::ReadFirstLane: return value.ArgumentCount() < 2 ? std::optional(false) : std::nullopt; + case IrOpcode::ReadConst: { + const auto* slot = value.ArgumentCount() < 2 || value.Argument(1) == nullptr ? nullptr : value.Argument(1)->Resolve(); + if (slot == nullptr || !slot->HasImmediate() || slot->Type() != IrType::U32 || slot->ImmediateU32() >= program.srtReads.size()) return false; + return std::nullopt; + } case IrOpcode::LoadAddressU32: case IrOpcode::ReadConstBuffer: return IsRawRead(program, value); case IrOpcode::Phi: return std::nullopt; @@ -354,10 +359,21 @@ std::optional evaluableAlone(const IrResourcePlan& program, const IrValue& } template -bool BasicEvaluator::MayEvaluate(const IrResourcePlan& program, IrValue* value) { +bool BasicEvaluator::MayEvaluate(const IrResourcePlan& program, IrValue* value, const IrValue* activeMask) { std::unordered_map known; - const auto requirements = [&program](IrValue* current) { + const auto takesActiveArm = [activeMask](const IrValue* current) { + return activeMask != nullptr && IsRuntimeSelect(current->Opcode()) && current->ArgumentCount() == 3 && current->Argument(0)->Resolve() == activeMask; + }; + const auto requirements = [&program, &takesActiveArm](IrValue* current) { std::vector required; + if (takesActiveArm(current)) { + required.push_back(current->Argument(1) != nullptr ? current->Argument(1)->Resolve() : nullptr); + return required; + } + if (current->Opcode() == IrOpcode::ReadConst) { + required.push_back(program.srtReads[current->Argument(1)->Resolve()->ImmediateU32()].value); + return required; + } if (current->IsPhi()) { required.push_back(ResolveInvariantPhi(program, current)); return required; @@ -384,15 +400,23 @@ bool BasicEvaluator::MayEvaluate(const IrResourcePlan& program, IrValue* } return required; }; + std::unordered_set expanding; std::vector> pending{{value->Resolve(), false}}; while (!pending.empty()) { const auto [current, expanded] = pending.back(); pending.pop_back(); if (known.contains(current)) continue; - if (const auto alone = evaluableAlone(program, *current)) { + if (current->Opcode() == IrOpcode::ReadFirstLane && current->ArgumentCount() >= 2) { + auto* source = current->Argument(0) != nullptr ? current->Argument(0)->Resolve() : nullptr; + const auto* mask = current->Argument(1) != nullptr ? current->Argument(1)->Resolve() : nullptr; + known.emplace(current, source != nullptr && MayEvaluate(program, source, mask)); + continue; + } + if (const auto alone = takesActiveArm(current) ? std::nullopt : evaluableAlone(program, *current)) { known.emplace(current, *alone); continue; } + if (!expanded && !expanding.insert(current).second) continue; const auto required = requirements(current); if (!expanded) { pending.emplace_back(current, true); @@ -401,7 +425,10 @@ bool BasicEvaluator::MayEvaluate(const IrResourcePlan& program, IrValue* } continue; } - known.emplace(current, std::ranges::all_of(required, [&known](const IrValue* argument) { return argument != nullptr && known.at(argument); })); + known.emplace(current, std::ranges::all_of(required, [&known](const IrValue* argument) { + const auto found = argument != nullptr ? known.find(argument) : known.end(); + return found != known.end() && found->second; + })); } return known.at(value->Resolve()); } diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 3833a11d11..d9e23dc9e6 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -155,7 +155,7 @@ The comparison runs again when any of them changes. A CPU store into unwatched m A cached shader keeps an `IrResourcePlan` (`ResourceMaterializer::ExtractPlan` in [`ResourceMaterializer.cpp`](../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp)) for as long as it is registered: a copy of the IR values its descriptor sources, SRT reads, uniform fill and control-flow conditions depend on, which every capture walks to read the shader's resources from guest memory. The copy is sized to what the walk can use: -- A control-flow condition is kept only when `Detail::Evaluator::MayEvaluate` finds a runtime it could be evaluated for. A condition that depends on a vector register, or on an opcode the evaluator does not evaluate, takes both successors whatever the runtime holds, so the plan stores it as no condition and does not copy its operands. Such conditions are most of a shader's IR when the masked-lane code feeds its branches. +- A control-flow condition is kept only when `Detail::Evaluator::MayEvaluate` finds a runtime it could be evaluated for. A condition that depends on a vector register, or on an opcode the evaluator does not evaluate, takes both successors whatever the runtime holds, so the plan stores it as no condition and does not copy its operands. A first-lane read is judged by its source under the read's active mask, and a constant read by the SRT read it names, as the evaluator reads them. Such conditions are most of a shader's IR when the masked-lane code feeds its branches. - The copied values are allocated in one `IrValueArena` and hold no use lists: the walk only follows arguments. ### Shader MODE register access