diff --git a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp index 02a244738..030146961 100644 --- a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp @@ -209,7 +209,7 @@ int main() { passed &= run("cross-lane reads must keep the select", [] { bool ok = true; - for (const IrOpcode opcode : {IrOpcode::ReadFirstLane, IrOpcode::DppMoveU32, IrOpcode::BpermuteU32, IrOpcode::Permlane16U32, IrOpcode::SwizzleU32}) { + for (const IrOpcode opcode : {IrOpcode::DppMoveU32, IrOpcode::BpermuteU32, IrOpcode::Permlane16U32, IrOpcode::SwizzleU32}) { Builder b; auto& exec = b.mask(16u); auto& old = b.lane(); @@ -227,6 +227,132 @@ int main() { return ok && b.eliminate() == 0u && !removed(written); }); + passed &= run("a first-lane read guarded at both ends loses the source select", [] { + Builder b; + auto& exec = b.mask(16u); + auto& narrow = b.logicalAnd(exec, b.mask(8u)); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + auto& read = b.emit(IrOpcode::ReadFirstLane, IrType::U32, {&written, &narrow}); + b.keep(b.select(narrow, read, old)); + return b.eliminate() == 1u && removed(written); + }); + + passed &= run("an unguarded first-lane result keeps the source select for empty exec", [] { + Builder b; + auto& exec = b.emit(IrOpcode::INotEqual32, IrType::Bool, {&b.lane(), &b.constant(0u)}); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + auto& empty = b.logicalAnd(exec, b.mask(0u)); + b.keep(b.emit(IrOpcode::ReadFirstLane, IrType::U32, {&written, &empty})); + return b.eliminate() == 0u && !removed(written); + }); + + passed &= run("a first-lane result read under the write mask keeps the select when read exec is narrower", [] { + Builder b; + auto& exec = b.emit(IrOpcode::INotEqual32, IrType::Bool, {&b.lane(), &b.constant(0u)}); + auto& empty = b.logicalAnd(exec, b.mask(0u)); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + auto& read = b.emit(IrOpcode::ReadFirstLane, IrType::U32, {&written, &empty}); + b.keep(b.select(exec, read, old)); + return b.eliminate() == 0u && !removed(written); + }); + + passed &= run("a first-lane read under a wider exec keeps the select", [] { + Builder b; + auto& wide = b.mask(32u); + auto& exec = b.logicalAnd(wide, b.mask(16u)); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + auto& read = b.emit(IrOpcode::ReadFirstLane, IrType::U32, {&written, &wide}); + b.keep(b.select(wide, read, old)); + return b.eliminate() == 0u && !removed(written); + }); + + const auto selectedExec = [](bool negate, bool widerArm) { + Builder b; + auto& wide = b.mask(32u); + auto& exec = b.logicalAnd(wide, b.mask(16u)); + auto& empty = b.logicalAnd(exec, b.mask(0u)); + auto& selector = negate ? b.emit(IrOpcode::LogicalNot, IrType::Bool, {&exec}) : exec; + auto& later = b.emit(IrOpcode::SelectU1, IrType::Bool, {&selector, negate ? (widerArm ? &wide : &empty) : &wide, negate ? &wide : (widerArm ? &wide : &empty)}); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + b.keep(b.select(later, b.add(written, 2u), old)); + return std::pair{b.eliminate(), removed(written)}; + }; + + passed &= run("selected boolean exec implies the write mask through its selector", [&] { + for (bool negate : {false, true}) { + const auto [count, gone] = selectedExec(negate, false); + if (count != 1u || !gone) return false; + } + return true; + }); + + passed &= run("selected boolean exec with a wider unguarded arm keeps the select", [&] { + for (bool negate : {false, true}) { + const auto [count, gone] = selectedExec(negate, true); + if (count != 0u || gone) return false; + } + return true; + }); + + const auto rebuiltExec = [](bool mismatchedHigh) { + Builder b; + const auto [low, high] = b.ballotWords(b.mask(16u)); + const auto [wideLow, wideHigh] = b.ballotWords(b.mask(48u)); + auto& exec = b.threadBit(*low, *high); + auto& later = b.threadBit(*low, mismatchedHigh ? *wideHigh : *high); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + b.keep(b.select(later, b.add(written, 2u), old)); + return std::pair{b.eliminate(), removed(written)}; + }; + + passed &= run("an exec rebuilt from the same words implies the write mask", [&] { + const auto [count, gone] = rebuiltExec(false); + return count >= 1u && gone; + }); + + passed &= run("matching low mask words alone do not imply a wave64 write mask", [&] { + const auto [count, gone] = rebuiltExec(true); + return count == 0u && !gone; + }); + + const auto firstLaneInLoop = [](bool carriedResult) { + Builder b; + auto& exec = b.mask(16u); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + IrBlock& entry = *b.block; + IrBlock& loop = b.newBlock(); + entry.AddBranch(&loop); + loop.AddBranch(&loop); + b.block = &loop; + auto& loopExec = b.logicalAnd(exec, b.mask(8u)); + auto& read = b.emit(IrOpcode::ReadFirstLane, IrType::U32, {&written, &loopExec}); + if (carriedResult) { + auto& carried = b.phi(IrType::U32, entry, old); + carried.AddPhiOperand(&loop, &read); + b.keep(b.select(loopExec, carried, old)); + } else { + b.keep(b.select(loopExec, read, old)); + } + return std::pair{b.eliminate(), removed(written)}; + }; + + passed &= run("a loop-local first-lane result guarded in its own iteration loses the source select", [&] { + const auto [count, gone] = firstLaneInLoop(false); + return count == 1u && gone; + }); + + passed &= run("a first-lane result carried to another loop iteration keeps the source select", [&] { + const auto [count, gone] = firstLaneInLoop(true); + return count == 0u && !gone; + }); + passed &= run("a branch condition derived from the write keeps the select", [] { Builder b; auto& exec = b.mask(16u); diff --git a/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp b/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp index 51ec69506..0ab716a84 100644 --- a/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp +++ b/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp @@ -382,6 +382,11 @@ public: result = true; } else if (condition->Opcode() == IrOpcode::LogicalAnd) { result = Implies(condition->Argument(0), depth + 1u) || Implies(condition->Argument(1), depth + 1u); + } else if (condition->Opcode() == IrOpcode::SelectU1) { + const IrValue* selector = condition->Argument(0)->Resolve(); + const bool trueCase = Implies(condition->Argument(1), depth + 1u) || Implies(selector, depth + 1u); + const bool falseCase = Implies(condition->Argument(2), depth + 1u) || (selector->Opcode() == IrOpcode::LogicalNot && Implies(selector->Argument(0), depth + 1u)); + result = trueCase && falseCase; } else if (condition->IsPhi()) { result = merges(*condition); for (std::size_t index = 0; result && index < condition->ArgumentCount(); ++index) { @@ -443,6 +448,8 @@ private: bool wordsImply(const IrValue* low, const IrValue* high, std::uint32_t depth) { if (isImmediate(low, 0u) && (waveSize == 32u || isImmediate(high, 0u))) return true; + const auto maskBit = threadBit(mask, waveSize); + if (maskBit && low == maskBit->low && (waveSize == 32u || high == maskBit->high)) return true; const ImplicationKey key{low, high}; if (const auto known = lookup(key)) return *known; if (depth > ImplicationDepthLimit || work == 0) return false; @@ -537,10 +544,11 @@ std::size_t walkLimit(const IrProgram& program) { class MaskedLaneWalk { public: - MaskedLaneWalk(const IrProgram& program, const std::vector& regions) : program(program), regions(regions), uniform(uniformEntryBlocks(program)), limit(walkLimit(program)), implications(regions, uniform, program.WaveSize(), work) {} + MaskedLaneWalk(const IrProgram& program, const std::vector& regions, std::uint32_t nesting = 0) : program(program), regions(regions), uniform(uniformEntryBlocks(program)), limit(walkLimit(program)), implications(regions, uniform, program.WaveSize(), work), nesting(nesting) {} bool UnobservedWhereMasked(const IrValue& select, const IrValue* mask) { const bool acrossLoops = mask->HasImmediate() || regionOf(regions, mask->Parent()) == Region::Once; + const bool sameIteration = !acrossLoops && select.Opcode() == IrOpcode::ReadFirstLane; work = limit; implications.Reset(mask, acrossLoops); const bool quadMasked = program.Resources().stage == IrShaderStage::Pixel && wholeQuadSource(mask, program.WaveSize()) != nullptr; @@ -553,10 +561,17 @@ public: pending.pop_back(); for (const IrUse& use : value->OperandUses()) { const IrValue* user = use.user; - if (!followable(regions, user->Parent(), acrossLoops)) return false; + if (sameIteration ? user->Parent() != select.Parent() || user->IsPhi() : !followable(regions, user->Parent(), acrossLoops)) return false; if (isSelect(user->Opcode()) && use.operand == 1u && implications.Implies(user->Argument(0))) continue; if (user->Opcode() == IrOpcode::LogicalAnd && use.operand < 2u && implications.Implies(user->Argument(1u - use.operand))) continue; if (readsOnlyWhereActive(user->Opcode()) && use.operand + 1u < user->ArgumentCount() && implications.Implies(user->Argument(user->ArgumentCount() - 1u))) continue; + if (user->Opcode() == IrOpcode::ReadFirstLane && use.operand == 0u && nesting < DepthLimit && implications.Implies(user->Argument(1))) { + MaskedLaneWalk nested(program, regions, nesting + 1u); + nested.limit = work; + const bool unobserved = nested.UnobservedWhereMasked(*user, user->Argument(1)->Resolve()); + work = nested.work; + if (unobserved) continue; + } if (!user->IsPhi() && !isLaneLocal(user->Opcode()) && !isExplicitLodSample(program, *user) && !(quadMasked && user->Opcode() == IrOpcode::ImageSampleRaw)) return false; if (firstVisit(*user)) { if (work == 0) return false; @@ -592,6 +607,7 @@ private: std::vector pending; std::vector visits; std::uint32_t epoch = 0; + std::uint32_t nesting = 0; }; } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 672d11da6..4648512e7 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -375,6 +375,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAudioInHqOpen, sceAudioInAsyncOpen](../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) open the same port as `sceAudioInOpen`, so `sceAudioInInput`, `sceAudioInGetSilentState` and `sceAudioInClose` accept their handles. The signatures agree across [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/fc5d2cc219c43c8c8f354153e7b3d0c04a5d506e/src/core/libraries/audio/audioin.cpp), [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Audio/AudioIn.cs) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/c86c8b4d9333a766cc8c4495f89f893bf35e6199/src/libs/audio.cpp). HqOpen is limited to 128 samples at 48 kHz; AsyncOpen rejects sizes above 384 and throws for currently unverified inputs. Host capture pacing and console behavior remain unverified. - [sem_timedwait](../../core/libs/prx/libkernel/Pthread/Posix/Sem.cpp) (libkernel) consumes an available count before checking the timeout and waits indefinitely for a null timeout, following FreeBSD 11's `sem_timedwait`. Console behavior for null and invalid timeouts when a count is available is unverified. +- [MaskedSelectEliminator](../../core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp) can discard inactive source lanes through `ReadFirstLane` only when its read mask implies the write mask and its result is unobserved outside the read mask. A loop-local read mask requires result uses in the same block without phi transport. The empty-EXEC lane-zero fallback follows AMD's [Vega ISA, section 12.8, V_READFIRSTLANE_B32, page 127](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/vega-shader-instruction-set-architecture.pdf) and the existing SPIR-V emitter. The optimization is covered by synthetic IR tests but has not been compared with console hardware; proofs stop at the existing work and nesting limits. - [getc_unlocked, putc_unlocked](../../core/libs/prx/libc/src/StandardStreams.cpp) (libc) - byte I/O uses the host unlocked operations; guest EOF/error flags and macro counters follow `FileStream`. Linux status queries use `feof_unlocked`/`ferror_unlocked` because [glibc `feof`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/feof.c) and [`ferror`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/ferror.c) can acquire the stream lock. Windows uses [the CRT no-lock byte operations](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/fgetc-nolock-fgetwc-nolock) and `feof`/`ferror`, verified under a held stream lock on UCRT. Console behavior and other host CRTs remain unverified.