Merge pull request #3739 from eljackjoestar-cmd/codex/masked-first-lane-proof
fix(shader): prove masked writes through guarded first-lane reads
This commit is contained in:
commit
7f7f961bf8
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<Region>& regions) : program(program), regions(regions), uniform(uniformEntryBlocks(program)), limit(walkLimit(program)), implications(regions, uniform, program.WaveSize(), work) {}
|
||||
MaskedLaneWalk(const IrProgram& program, const std::vector<Region>& 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<const IrValue*> pending;
|
||||
std::vector<std::uint32_t> visits;
|
||||
std::uint32_t epoch = 0;
|
||||
std::uint32_t nesting = 0;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue