From bf0589e2824e6408dcfce1cd2df4fa64408b6800 Mon Sep 17 00:00:00 2001 From: battleblob1 Date: Wed, 21 Oct 2026 23:09:27 +0300 Subject: [PATCH] fix(recompiler): accept optional-body merges implied by the skip predicate in stencil replay An EXEC- or VCC-skipped optional body is accepted when every merge phi's value from the body equals the skipped value on lanes inactive in the body. A body value that implies the skip predicate is false on every inactive lane, so it equals the skipped value when that value is the predicate itself. inactiveEquals now accepts that case, which covers a lane's own bit of a ballot of the predicate merged with the predicate. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/tests/MaskedSelect.cpp | 32 +++++++++++++++++++ core/shader/recompiler/StencilReplay.cpp | 1 + 2 files changed, 33 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp index 84be667ce8..606af7d757 100644 --- a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp @@ -1236,6 +1236,38 @@ int main() { return StencilReplaySafe(b.program); }; + const auto optionalPredicateBody = [&](bool negated) { + Builder b; + b.program.Resources().stage = IrShaderStage::Pixel; + auto& entry = *b.block; + auto& predicate = pixelPredicate(b); + auto& condition = b.emit(IrOpcode::LogicalNot, IrType::Bool, {&predicate}); + auto& body = b.newBlock(); + auto& merge = b.newBlock(); + entry.AddBranch(&body); + entry.AddBranch(&merge); + body.AddBranch(&merge); + auto& entryInfo = b.program.Metadata().blockInfo[0u]; + entryInfo.terminator.kind = TerminatorKind::ConditionalBranch; + entryInfo.terminator.condition = BranchCondition::ExecZero; + entryInfo.terminator.trueBlock = merge.Id(); + entryInfo.terminator.falseBlock = body.Id(); + entryInfo.condition = &condition; + auto& bodyInfo = b.program.Metadata().blockInfo[1u]; + bodyInfo.terminator.kind = TerminatorKind::Branch; + bodyInfo.terminator.trueBlock = merge.Id(); + b.block = &body; + auto& written = negated ? b.emit(IrOpcode::LogicalNot, IrType::Bool, {&predicate}) : b.ballotBit(predicate); + b.block = &merge; + auto& merged = b.phi(IrType::Bool, entry, predicate); + merged.AddPhiOperand(&body, &written); + b.keep(b.select(merged, b.constant(1u), b.constant(0u))); + exportUnder(b, b.truth(), true, true); + return StencilReplaySafe(b.program); + }; + + passed &= run("an optional wave body may merge a lane's own bit of the predicate's ballot with the skipped predicate", [&] { return optionalPredicateBody(false); }); + passed &= run("an optional wave body cannot merge a value that differs from the skipped predicate on inactive lanes", [&] { return !optionalPredicateBody(true); }); passed &= run("an optional wave body preserves inactive values consumed only within a lane", [&] { return optionalPixelBody(false); }); passed &= run("an optional wave body cannot change helper coordinates for later implicit samples", [&] { return !optionalPixelBody(true); }); passed &= run("an optional wave body cannot change helper coordinates for LOD queries", [&] { return !optionalPixelBody(true, IrOpcode::ImageQueryLod); }); diff --git a/core/shader/recompiler/StencilReplay.cpp b/core/shader/recompiler/StencilReplay.cpp index b72ffe0e3e..39acca62dc 100644 --- a/core/shader/recompiler/StencilReplay.cpp +++ b/core/shader/recompiler/StencilReplay.cpp @@ -497,6 +497,7 @@ private: if (depth > 32u) return false; value = value->Resolve(); if (sameValue(value, other)) return true; + if (value->Type() == IrType::Bool && other->Resolve() == predicate->Resolve() && GuardImplies(program, value, predicate)) return true; if ((value->Opcode() == IrOpcode::SelectU32 || value->Opcode() == IrOpcode::SelectF32 || value->Opcode() == IrOpcode::SelectU1) && value->ArgumentCount() == 3u && (value->Argument(0)->Resolve() == predicate->Resolve() || GuardImplies(program, value->Argument(0), predicate))) return inactiveEquals(value->Argument(2), other, predicate, region, depth + 1u); if (value->IsPhi() && region.contains(value->Parent()) && value->ArgumentCount() != 0u && value->ArgumentCount() == value->PhiBlockCount()) { return std::all_of(value->Arguments().begin(), value->Arguments().end(), [&](const IrValue* argument) { return inactiveEquals(argument, other, predicate, region, depth + 1u); });