shader: eliminate predicates for empty branch detours

This commit is contained in:
nmzik
2026-10-18 20:37:39 +02:00
parent e9bcc77e0b
commit 8b51370ca1
2 changed files with 107 additions and 24 deletions
@@ -30,32 +30,62 @@ void RemoveIdentities(Program& program) {
}
void EliminateDeadCode(const BlockList& blocks) {
std::vector<Inst*> pending;
for (auto* block: blocks) {
for (auto& inst: *block) {
// Retained resource-planning instructions live outside the block list.
inst.live = inst.MayHaveSideEffects() || std::ranges::any_of(inst.Uses(),
[](const Use& use) { return use.user->Parent() == nullptr; });
if (inst.live) pending.push_back(&inst);
}
}
while (!pending.empty()) {
const auto* inst = pending.back();
pending.pop_back();
for (size_t arg = 0; arg < inst->NumArgs(); ++arg) {
auto* input = inst->Arg(arg).TryInstruction();
if (input != nullptr && input->Parent() != nullptr && !input->live) {
input->live = true;
pending.push_back(input);
bool changed;
do {
std::vector<Inst*> pending;
for (auto* block: blocks) {
for (auto& inst: *block) {
// Retained resource-planning instructions live outside the block list.
inst.live = inst.MayHaveSideEffects() || std::ranges::any_of(inst.Uses(),
[](const Use& use) { return use.user->Parent() == nullptr; });
if (inst.live) pending.push_back(&inst);
}
}
}
// Unlink every dead operand before freeing nodes in a dead Phi cycle.
for (auto* block: blocks)
for (auto& inst: *block)
if (!inst.live) inst.Invalidate();
for (auto* block: blocks)
std::erase_if(block->Instructions(), [](const Inst& inst) { return !inst.live; });
while (!pending.empty()) {
const auto* inst = pending.back();
pending.pop_back();
for (size_t arg = 0; arg < inst->NumArgs(); ++arg) {
auto* input = inst->Arg(arg).TryInstruction();
if (input != nullptr && input->Parent() != nullptr && !input->live) {
input->live = true;
pending.push_back(input);
}
}
}
// Unlink every dead operand before freeing nodes in a dead Phi cycle.
for (auto* block: blocks)
for (auto& inst: *block)
if (!inst.live) inst.Invalidate();
for (auto* block: blocks)
std::erase_if(block->Instructions(), [](const Inst& inst) { return !inst.live; });
changed = false;
for (auto* block: blocks) {
const auto& term = block->terminator;
if (term.kind != CFG::TerminatorKind::ConditionalBranch || term.loop_header ||
block->condition.IsImmediate()) continue;
for (const bool take_true: {false, true}) {
const auto* empty = take_true ? term.false_block : term.true_block;
const auto* merge = take_true ? term.true_block : term.false_block;
if (empty == nullptr || merge == nullptr || !empty->empty() ||
empty->terminator.loop_header || merge->terminator.loop_header ||
empty->terminator.kind != CFG::TerminatorKind::Branch ||
empty->terminator.true_block != merge ||
std::ranges::any_of(*merge, [](const Inst& inst) {
return inst.GetOpcode() == ValueOpcode::Phi;
})) continue;
// Keep the CFG and SSA edges intact; only the empty detour's predicate is dead.
for (auto& inst: *block) {
if (inst.GetOpcode() == ValueOpcode::Reference &&
inst.Arg(0).Resolve() == block->condition.Resolve())
inst.SetArg(0, Value(take_true));
}
block->condition = Value(take_true);
changed = true;
break;
}
}
} while (changed);
}
} // namespace Libs::Graphics::ShaderRecompiler::IR
+53
View File
@@ -15353,6 +15353,58 @@ void TestScalarMemoryLoadCrossesIntoVcc() {
}
}
void TestEmptyBranchScalarReadDce() {
using namespace ShaderRecompiler::IR;
using ShaderRecompiler::CFG::TerminatorKind;
for (const bool reverse : {false, true}) {
for (uint32_t mode = 0; mode < 4; ++mode) {
Block entry, detour, merge;
const BlockList blocks{&entry, &detour, &merge};
entry.AddBranch(&detour);
entry.AddBranch(&merge);
detour.AddBranch(&merge);
entry.terminator.kind = TerminatorKind::ConditionalBranch;
entry.terminator.true_block = reverse ? &merge : &detour;
entry.terminator.false_block = reverse ? &detour : &merge;
entry.terminator.merge_block = &merge;
detour.terminator.kind = TerminatorKind::Branch;
detour.terminator.true_block = &merge;
IREmitter ir(&entry);
const auto address = ir.Emit(ValueOpcode::GetAddressResource,
{Value(0x1000u), Value(0u)});
const auto read = ir.Emit(ValueOpcode::LoadAddressU32,
{address, Value(24u), Value(0u), Value(true)});
const auto predicate = ir.INotEqual(U32(read), U32(Value(0u)));
entry.condition = ir.Emit(ValueOpcode::ConditionRef, {predicate});
ir.Emit(ValueOpcode::Reference, {entry.condition});
ir.Emit(ValueOpcode::Barrier);
detour.AppendNewInst(ValueOpcode::ControlNop, {});
if (mode == 1) detour.AppendNewInst(ValueOpcode::Barrier, {});
if (mode == 2) {
auto& phi = merge.AppendNewInst(ValueOpcode::Phi, {});
phi.SetFlags(Type::U32);
phi.AddPhiOperand(&entry, Value(1u));
phi.AddPhiOperand(&detour, Value(2u));
merge.AppendNewInst(ValueOpcode::ReferenceU32, {Value(&phi)});
}
if (mode == 3) merge.AppendNewInst(ValueOpcode::ReferenceU32, {read});
EliminateDeadCode(blocks);
Check(entry.condition.IsImmediate() == (mode == 0 || mode == 3),
"empty branch cleanup changed side effects or a live Phi");
const auto contains = [&](ValueOpcode op) {
return std::ranges::any_of(entry, [=](const Inst& inst) {
return inst.GetOpcode() == op;
});
};
Check(contains(ValueOpcode::LoadAddressU32) == (mode != 0),
"empty branch cleanup lost a shared read or retained a dead read");
Check(contains(ValueOpcode::Barrier) && entry.ImmSuccessors().size() == 2 &&
merge.ImmPredecessors().size() == 2,
"empty branch cleanup changed earlier side effects or CFG edges");
}
}
}
void TestScalarMemoryUnusedTailDce() {
const uint32_t shader[] = {
EncodeSmem0(0x0c, 16, 0),
@@ -16701,6 +16753,7 @@ int main() {
TestSrtWalkerRealSBufferTranslation();
TestScalarMemorySourcesCapturedBeforeWrites();
TestScalarMemoryLoadCrossesIntoVcc();
TestEmptyBranchScalarReadDce();
TestScalarMemoryUnusedTailDce();
TestResourceTrackingRealDensePatching();
TestDirectTranslationResetsAnalysisState();