From 60c655211d5575da28ccbea8a65126fdaeca307e Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Mon, 5 Oct 2026 14:10:02 +1300 Subject: [PATCH] perf(recompiler): let exec-guarded memory and exports end a masked select A register written under EXEC is usually consumed by an LDS or buffer access or an export under the same EXEC. MaskedSelectEliminator kept the select in that case, which leaves the NGG mesh shaders where they were. The emitter performs buffer and LDS loads and stores (8 to 128 bits, GPU-selected V# included) and SetAttribute only in lanes whose active operand is true: stores and exports sit in an if on it, loads return 0 outside it. Every other operand is read only in those lanes. So such an access whose active operand implies the select's condition reads the derived value only where it equals the original, and it now ends the search like a masked select does. The active operand itself, atomics, append/consume, scratch and address-space accesses and image accesses still keep the select. --- .../libSceAgcDriver/tests/MaskedSelect.cpp | 38 +++++++++++++++++++ .../src/MaskedSelectEliminator.cpp | 34 +++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp index 8e1cd2e3f..119f87925 100644 --- a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp @@ -336,5 +336,43 @@ int main() { return sample(IrShaderStage::Pixel, RdnaImageSampleFlagLevelZero) && sample(IrShaderStage::Mesh, 0u); }); + const auto guarded = [](IrOpcode opcode, bool sameExec, bool asActive) { + Builder b; + auto& wide = b.mask(48u); + auto& exec = b.logicalAnd(wide, b.mask(16u)); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + IrValue& active = asActive ? b.emit(IrOpcode::INotEqual32, IrType::Bool, {&written, &b.constant(0u)}) : sameExec ? exec : wide; + switch (opcode) { + case IrOpcode::WriteSharedU32: + b.emit(opcode, IrType::Void, {&old, &written, &active}); + break; + case IrOpcode::LoadBufferU32: + b.keep(b.select(exec, b.emit(opcode, IrType::U32, {&b.emit(IrOpcode::GetBufferResource, IrType::BufferResource, {}), &written, &b.constant(0u), &b.constant(0u), &active}), old)); + break; + default: + b.emit(opcode, IrType::Void, {&b.emit(IrOpcode::CompositeConstructU32x4, IrType::U32x4, {&written, &old, &old, &old}), &active}); + break; + } + b.eliminate(); + return removed(written); + }; + + passed &= run("a store, load or export whose exec implies the write's exec drops the select", [&] { + return guarded(IrOpcode::WriteSharedU32, true, false) && guarded(IrOpcode::LoadBufferU32, true, false) && guarded(IrOpcode::SetAttribute, true, false); + }); + + passed &= run("a store, load or export under a wider exec keeps the select", [&] { + return !guarded(IrOpcode::WriteSharedU32, false, false) && !guarded(IrOpcode::LoadBufferU32, false, false) && !guarded(IrOpcode::SetAttribute, false, false); + }); + + passed &= run("a value used as a store's exec keeps the select", [&] { + return !guarded(IrOpcode::WriteSharedU32, true, true); + }); + + passed &= run("an atomic keeps the select", [&] { + return !guarded(IrOpcode::SharedAtomicIAdd32, true, false); + }); + return passed ? 0 : 1; } diff --git a/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp b/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp index 6ab6062c4..74f4cda6f 100644 --- a/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp +++ b/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp @@ -175,6 +175,39 @@ bool isLaneLocal(IrOpcode opcode) { } } +bool readsOnlyWhereActive(IrOpcode opcode) { + switch (opcode) { + case IrOpcode::LoadBufferU8: + case IrOpcode::LoadBufferU16: + case IrOpcode::LoadBufferU32: + case IrOpcode::LoadBufferU32x2: + case IrOpcode::LoadBufferU32x3: + case IrOpcode::LoadBufferU32x4: + case IrOpcode::StoreBufferU8: + case IrOpcode::StoreBufferU16: + case IrOpcode::StoreBufferU32: + case IrOpcode::StoreBufferU32x2: + case IrOpcode::StoreBufferU32x3: + case IrOpcode::StoreBufferU32x4: + case IrOpcode::LoadSharedU8: + case IrOpcode::LoadSharedU16: + case IrOpcode::LoadSharedU32: + case IrOpcode::LoadSharedU32x2: + case IrOpcode::LoadSharedU32x3: + case IrOpcode::LoadSharedU32x4: + case IrOpcode::WriteSharedU8: + case IrOpcode::WriteSharedU16: + case IrOpcode::WriteSharedU32: + case IrOpcode::WriteSharedU32x2: + case IrOpcode::WriteSharedU32x3: + case IrOpcode::WriteSharedU32x4: + case IrOpcode::SetAttribute: + return true; + default: + return false; + } +} + bool isExplicitLodSample(const IrProgram& program, const IrValue& value) { if (value.Opcode() != IrOpcode::ImageSampleRaw) return false; if (program.Resources().stage != IrShaderStage::Pixel) return true; @@ -316,6 +349,7 @@ bool unobservedWhereMasked(const IrProgram& program, const std::unordered_setParent() == nullptr || !once.contains(user->Parent())) return false; if (isSelect(user->Opcode()) && use.operand == 1u && implies(user->Argument(0), mask, waveSize)) continue; if (user->Opcode() == IrOpcode::LogicalAnd && use.operand < 2u && implies(user->Argument(1u - use.operand), mask, waveSize)) continue; + if (readsOnlyWhereActive(user->Opcode()) && use.operand + 1u < user->ArgumentCount() && implies(user->Argument(user->ArgumentCount() - 1u), mask, waveSize)) continue; if (!user->IsPhi() && !isLaneLocal(user->Opcode()) && !isExplicitLodSample(program, *user)) return false; if (visited.insert(user).second) { if (visited.size() > VisitLimit) return false;