Files
AnyPS5/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp
T
Dean Galvin d37463e76f fix(shader): prove masked lanes unread for every select in one backward pass
MaskedSelectEliminator walked forward from each select to see whether the
lanes its mask excludes are read. Since fda4aa90 the walks pass explicit-LOD
gathers, size queries and guarded image writes, so in a large pixel shader
most of them cover the rest of the program, and the cost grows with the
square of its size (#3504).

The pass now computes, for every value, the set of masks under which it is
unread, from its uses backwards: a use guarded by a condition contributes the
masks that condition implies, a lane-local use adds what its result is unread
under, and any other use contributes nothing. A select is dropped when its
mask is in its own set. Loops are solved as strongly connected components.
The rules for guards, lane-local uses, loops, whole quad masks, selected
execs, rebuilt mask words and guarded first-lane reads are the ones the walk
applied. A first-lane read passes the masks its read mask implies when its
own result is unread outside that read mask, which is solved once per read in
the same way. The walk's step limit, its 64-level implication depth and its
first-lane nesting depth are gone, so a proof no longer depends on the order
of the questions.
2026-10-12 21:16:29 +13:00

944 lines
41 KiB
C++

#include "IntermediateRepresentation/IrProgram.hpp"
#include "Optimization/MaskedSelectEliminator.hpp"
#include "RdnaDecoder/RdnaInstruction.hpp"
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <exception>
#include <functional>
#include <initializer_list>
#include <utility>
#include <vector>
using namespace ShaderRecompiler;
namespace {
struct Builder {
IrProgram program;
IrBlock* block = nullptr;
Builder() {
block = &newBlock();
program.SetEntryBlock(*block);
}
IrBlock& newBlock() {
auto& created = program.CreateBlock();
program.BlockOrder().push_back(&created);
return created;
}
IrValue& constant(std::uint32_t value) {
auto& created = program.CreateValue(IrOpcode::Void, IrType::U32);
created.SetImmediateU32(value);
return created;
}
IrValue& truth() {
auto& created = program.CreateValue(IrOpcode::Void, IrType::Bool);
created.SetImmediateBool(true);
return created;
}
IrValue& emit(IrOpcode opcode, IrType type, std::initializer_list<IrValue*> arguments, std::uint64_t flags = 0) {
auto& created = program.CreateValue(opcode, type, flags);
for (auto* argument : arguments) created.AddArgument(argument);
block->AppendInstruction(&created);
return created;
}
IrValue& lane() {
return emit(IrOpcode::LaneId, IrType::U32, {});
}
IrValue& mask(std::uint32_t lanes) {
return emit(IrOpcode::ULessThan32, IrType::Bool, {&lane(), &constant(lanes)});
}
IrValue& select(IrValue& condition, IrValue& active, IrValue& inactive) {
return emit(IrOpcode::SelectU32, IrType::U32, {&condition, &active, &inactive});
}
IrValue& add(IrValue& value, std::uint32_t immediate) {
return emit(IrOpcode::IAdd32, IrType::U32, {&value, &constant(immediate)});
}
IrValue& logicalAnd(IrValue& left, IrValue& right) {
return emit(IrOpcode::LogicalAnd, IrType::Bool, {&left, &right});
}
IrValue& threadBit(IrValue& low, IrValue& high) {
auto& laneId = lane();
auto& word = select(emit(IrOpcode::ULessThan32, IrType::Bool, {&laneId, &constant(32u)}), low, high);
auto& shift = emit(IrOpcode::BitwiseAnd32, IrType::U32, {&laneId, &constant(31u)});
auto& shifted = emit(IrOpcode::ShiftRightLogical32, IrType::U32, {&word, &shift});
auto& bit = emit(IrOpcode::BitwiseAnd32, IrType::U32, {&shifted, &constant(1u)});
return emit(IrOpcode::INotEqual32, IrType::Bool, {&bit, &constant(0u)});
}
IrValue& ballotBit(IrValue& predicate) {
auto& ballot = emit(IrOpcode::Ballot, IrType::U32x4, {&predicate});
auto& low = emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&ballot, &constant(0u)});
auto& high = emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&ballot, &constant(1u)});
return threadBit(low, high);
}
std::pair<IrValue*, IrValue*> ballotWords(IrValue& predicate) {
auto& ballot = emit(IrOpcode::Ballot, IrType::U32x4, {&predicate});
return {&emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&ballot, &constant(0u)}), &emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&ballot, &constant(1u)})};
}
IrValue& wordAnd(IrValue& left, IrValue& right) {
return emit(IrOpcode::BitwiseAnd32, IrType::U32, {&left, &right});
}
IrValue& phi(IrType type, IrBlock& predecessor, IrValue& incoming) {
auto& created = program.CreateValue(IrOpcode::Phi, type);
block->AppendInstruction(&created);
created.AddPhiOperand(&predecessor, &incoming);
return created;
}
void keep(IrValue& value) {
(void)emit(IrOpcode::ReferenceU32, IrType::Void, {&value});
}
std::uint32_t eliminate() {
return MaskedSelectEliminator{}.Eliminate(program).removedSelects;
}
};
bool removed(const IrValue& value) {
return value.Parent() == nullptr;
}
bool expect(const char* name, bool condition) {
if (!condition) std::fprintf(stderr, "%s\n", name);
return condition;
}
bool run(const char* name, const std::function<bool()>& test) {
try {
return expect(name, test());
} catch (const std::exception& error) {
std::fprintf(stderr, "%s: %s\n", name, error.what());
return false;
}
}
}
int main() {
bool passed = true;
passed &= run("a write read only under its own exec must lose its select", [] {
Builder b;
auto& exec = b.mask(16u);
auto& old = b.lane();
auto& written = b.select(exec, b.add(old, 1u), old);
auto& next = b.select(exec, b.add(written, 2u), old);
b.keep(next);
return b.eliminate() == 1u && removed(written) && !removed(next);
});
passed &= run("a write read under a wider exec must keep its 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& next = b.select(wide, b.add(written, 2u), old);
b.keep(next);
return b.eliminate() == 0u && !removed(written);
});
passed &= run("a write read under a narrower exec must lose its select", [] {
Builder b;
auto& exec = b.mask(32u);
auto& narrow = b.logicalAnd(exec, b.mask(16u));
auto& old = b.lane();
auto& written = b.select(exec, b.add(old, 1u), old);
auto& next = b.select(narrow, b.add(written, 2u), old);
b.keep(next);
return b.eliminate() == 1u && removed(written);
});
passed &= run("a write whose old lanes flow into the next write must keep its select", [] {
Builder b;
auto& exec = b.mask(16u);
auto& old = b.lane();
auto& written = b.select(exec, b.add(old, 1u), old);
auto& next = b.select(exec, b.add(old, 2u), written);
b.keep(next);
return b.eliminate() == 0u && !removed(written);
});
passed &= run("a chain of writes under one exec must collapse once the last one goes", [] {
Builder b;
auto& exec = b.mask(16u);
auto& old = b.lane();
auto& first = b.select(exec, b.add(old, 1u), old);
auto& second = b.select(exec, b.add(first, 2u), first);
auto& sink = b.select(exec, b.add(second, 3u), old);
b.keep(sink);
return b.eliminate() == 2u && removed(first) && removed(second) && !removed(sink);
});
passed &= run("a write read through thousands of lane-local values still loses its select", [] {
Builder b;
auto& exec = b.mask(16u);
auto& old = b.lane();
auto& written = b.select(exec, b.add(old, 1u), old);
IrValue* value = &written;
for (std::uint32_t step = 0; step < 6000u; ++step) value = &b.add(*value, step);
b.keep(b.select(exec, *value, old));
return b.eliminate() == 1u && removed(written);
});
passed &= run("thousands of lane-local values ending in a wider read keep 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);
IrValue* value = &written;
for (std::uint32_t step = 0; step < 6000u; ++step) value = &b.add(*value, step);
b.keep(b.select(wide, *value, old));
return b.eliminate() == 0u && !removed(written);
});
passed &= run("cross-lane reads must keep the select", [] {
bool ok = true;
for (const IrOpcode opcode : {IrOpcode::DppMoveU32, IrOpcode::BpermuteU32, IrOpcode::Permlane16U32, IrOpcode::SwizzleU32}) {
Builder b;
auto& exec = b.mask(16u);
auto& old = b.lane();
auto& written = b.select(exec, b.add(old, 1u), old);
auto& read = b.emit(opcode, IrType::U32, {&written, &exec});
b.keep(b.select(exec, read, old));
ok &= b.eliminate() == 0u && !removed(written);
}
Builder b;
auto& exec = b.mask(16u);
auto& old = b.lane();
auto& written = b.select(exec, b.add(old, 1u), old);
auto& ballot = b.emit(IrOpcode::Ballot, IrType::U32x4, {&b.emit(IrOpcode::INotEqual32, IrType::Bool, {&written, &b.constant(0u)})});
b.keep(b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&ballot, &b.constant(0u)}));
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);
auto& old = b.lane();
auto& written = b.select(exec, b.add(old, 1u), old);
auto& taken = b.emit(IrOpcode::INotEqual32, IrType::Bool, {&written, &b.constant(0u)});
(void)b.emit(IrOpcode::Reference, IrType::Void, {&taken});
return b.eliminate() == 0u && !removed(written);
});
passed &= run("a compare masked by the same exec must not keep the select", [] {
Builder b;
auto& exec = b.mask(16u);
auto& old = b.lane();
auto& written = b.select(exec, b.add(old, 1u), old);
auto& compare = b.logicalAnd(exec, b.emit(IrOpcode::INotEqual32, IrType::Bool, {&written, &b.constant(0u)}));
auto& ballot = b.emit(IrOpcode::Ballot, IrType::U32x4, {&compare});
b.keep(b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&ballot, &b.constant(0u)}));
return b.eliminate() == 1u && removed(written);
});
passed &= run("an exec rebuilt from a ballot of a narrower mask implies the write's exec", [] {
Builder b;
auto& exec = b.mask(48u);
auto& old = b.lane();
auto& written = b.select(exec, b.add(old, 1u), old);
auto& later = b.ballotBit(b.logicalAnd(exec, b.mask(40u)));
auto& next = b.select(later, b.add(written, 2u), old);
b.keep(next);
return b.eliminate() == 1u && removed(written);
});
passed &= run("an exec rebuilt from a ballot of a wider mask keeps the select", [] {
Builder b;
auto& wide = b.mask(48u);
auto& exec = b.logicalAnd(wide, b.mask(40u));
auto& old = b.lane();
auto& written = b.select(exec, b.add(old, 1u), old);
auto& later = b.ballotBit(wide);
b.keep(b.select(later, b.add(written, 2u), old));
return b.eliminate() == 0u && !removed(written);
});
passed &= run("a select under the full wave's exec must go whatever reads it", [] {
Builder b;
auto& full = b.threadBit(b.constant(0xffffffffu), b.constant(0xffffffffu));
auto& old = b.lane();
auto& written = b.select(full, b.add(old, 1u), old);
b.keep(written);
return b.eliminate() != 0u && removed(written);
});
passed &= run("a partial wave's exec is not the full wave", [] {
Builder b;
auto& partial = b.threadBit(b.constant(0xffffffffu), b.constant(0x0000ffffu));
auto& old = b.lane();
auto& written = b.select(partial, b.add(old, 1u), old);
b.keep(written);
return b.eliminate() == 0u && !removed(written);
});
const auto diamond = [](bool wideRead) {
Builder b;
auto& wide = b.mask(48u);
auto& exec = b.logicalAnd(wide, b.mask(16u));
auto& old = b.lane();
IrBlock& head = *b.block;
IrBlock& body = b.newBlock();
IrBlock& merge = b.newBlock();
head.AddBranch(&body);
head.AddBranch(&merge);
body.AddBranch(&merge);
b.block = &body;
auto& written = b.select(exec, b.add(old, 1u), old);
b.block = &merge;
auto& phi = b.program.CreateValue(IrOpcode::Phi, IrType::U32);
merge.AppendInstruction(&phi);
phi.AddPhiOperand(&head, &old);
phi.AddPhiOperand(&body, &written);
b.keep(b.select(wideRead ? wide : exec, b.add(phi, 2u), old));
return std::pair{b.eliminate(), removed(written)};
};
passed &= run("a write merged by a phi and read under its own exec must lose its select", [&] {
const auto [count, gone] = diamond(false);
return count == 1u && gone;
});
passed &= run("a write merged by a phi and read under the wider exec must keep its select", [&] {
const auto [count, gone] = diamond(true);
return count == 0u && !gone;
});
const auto mergedExec = [](bool wideBranch, bool loopMerge) {
Builder b;
auto& wide = b.mask(48u);
auto& exec = b.logicalAnd(wide, b.mask(32u));
auto& old = b.lane();
auto& written = b.select(exec, b.add(old, 1u), old);
IrBlock& head = *b.block;
IrBlock& body = b.newBlock();
IrBlock& merge = b.newBlock();
head.AddBranch(&body);
head.AddBranch(&merge);
body.AddBranch(&merge);
b.block = &body;
auto& killed = b.logicalAnd(wideBranch ? wide : exec, b.mask(16u));
auto& phi = b.program.CreateValue(IrOpcode::Phi, IrType::Bool);
merge.AppendInstruction(&phi);
phi.AddPhiOperand(&head, &exec);
phi.AddPhiOperand(&body, &killed);
if (loopMerge) {
IrBlock& exit = b.newBlock();
merge.AddBranch(&merge);
merge.AddBranch(&exit);
phi.AddPhiOperand(&merge, &phi);
b.block = &exit;
} else {
b.block = &merge;
}
b.keep(b.select(phi, b.add(written, 2u), old));
return std::pair{b.eliminate(), removed(written)};
};
passed &= run("an exec merged by a phi whose every incoming exec implies the write's exec drops the select", [&] {
const auto [count, gone] = mergedExec(false, false);
return count == 1u && gone;
});
passed &= run("an exec merged by a phi with a wider incoming exec keeps the select", [&] {
const auto [count, gone] = mergedExec(true, false);
return count == 0u && !gone;
});
passed &= run("an exec carried around a loop whose every incoming exec implies the write's exec drops the select", [&] {
const auto [count, gone] = mergedExec(false, true);
return count == 1u && gone;
});
passed &= run("an exec carried around a loop with a wider incoming exec keeps the select", [&] {
const auto [count, gone] = mergedExec(true, true);
return count == 0u && !gone;
});
const auto mergedWords = [](bool wideBranch) {
Builder b;
auto& wide = b.mask(48u);
auto& exec = b.logicalAnd(wide, b.mask(40u));
auto& old = b.lane();
auto& written = b.select(exec, b.add(old, 1u), old);
auto& saved = b.emit(IrOpcode::Ballot, IrType::U32x4, {&exec});
auto& savedLow = b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&saved, &b.constant(0u)});
auto& savedHigh = b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&saved, &b.constant(1u)});
IrBlock& head = *b.block;
IrBlock& body = b.newBlock();
IrBlock& merge = b.newBlock();
head.AddBranch(&body);
head.AddBranch(&merge);
body.AddBranch(&merge);
b.block = &body;
auto& survived = b.emit(IrOpcode::Ballot, IrType::U32x4, {&b.logicalAnd(wideBranch ? wide : exec, b.mask(24u))});
auto& survivedLow = b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&survived, &b.constant(0u)});
auto& survivedHigh = b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&survived, &b.constant(1u)});
auto& low = b.program.CreateValue(IrOpcode::Phi, IrType::U32);
auto& high = b.program.CreateValue(IrOpcode::Phi, IrType::U32);
merge.AppendInstruction(&low);
merge.AppendInstruction(&high);
low.AddPhiOperand(&head, &savedLow);
low.AddPhiOperand(&body, &survivedLow);
high.AddPhiOperand(&head, &savedHigh);
high.AddPhiOperand(&body, &survivedHigh);
b.block = &merge;
b.keep(b.select(b.threadBit(low, high), b.add(written, 2u), old));
return std::pair{b.eliminate(), removed(written)};
};
passed &= run("an exec rebuilt from mask words merged by phis implies the write's exec", [&] {
const auto [count, gone] = mergedWords(false);
return count == 1u && gone;
});
passed &= run("an exec rebuilt from merged mask words with a wider incoming ballot keeps the select", [&] {
const auto [count, gone] = mergedWords(true);
return count == 0u && !gone;
});
const auto chained = [](bool wideRoot, std::uint32_t kills) {
Builder b;
auto& wide = b.mask(48u);
auto& exec = b.logicalAnd(wide, b.mask(32u));
auto& old = b.lane();
auto& written = b.select(exec, b.add(old, 1u), old);
IrValue* later = wideRoot ? &wide : &exec;
for (std::uint32_t kill = 0; kill < kills; ++kill) later = &b.logicalAnd(*later, b.mask(31u - kill % 24u));
b.keep(b.select(*later, b.add(written, 2u), old));
return std::pair{b.eliminate(), removed(written)};
};
passed &= run("an exec narrowed by a long chain of kills still implies the write's exec", [&] {
const auto [count, gone] = chained(false, 24u);
return count == 1u && gone;
});
passed &= run("a long chain of kills rooted at a wider exec keeps the select", [&] {
const auto [count, gone] = chained(true, 24u);
return count == 0u && !gone;
});
passed &= run("an exec narrowed by hundreds of kills still implies the write's exec", [&] {
const auto [count, gone] = chained(false, 300u);
return count == 1u && gone;
});
passed &= run("hundreds of kills rooted at a wider exec keep the select", [&] {
const auto [count, gone] = chained(true, 300u);
return count == 0u && !gone;
});
passed &= run("a write in a loop must keep its select", [] {
Builder b;
auto& exec = b.mask(16u);
auto& old = b.lane();
IrBlock& head = *b.block;
IrBlock& loop = b.newBlock();
IrBlock& exit = b.newBlock();
head.AddBranch(&loop);
loop.AddBranch(&loop);
loop.AddBranch(&exit);
b.block = &loop;
auto& written = b.select(exec, b.add(old, 1u), old);
auto& next = b.select(exec, b.add(written, 2u), old);
b.keep(next);
return b.eliminate() == 0u && !removed(written);
});
const auto laterLoop = [](bool wideRead) {
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);
IrBlock& head = *b.block;
IrBlock& loop = b.newBlock();
head.AddBranch(&loop);
loop.AddBranch(&loop);
b.block = &loop;
b.keep(b.select(wideRead ? wide : exec, b.add(written, 2u), old));
return std::pair{b.eliminate(), removed(written)};
};
passed &= run("a write read inside a later loop under its own exec loses its select", [&] {
const auto [count, gone] = laterLoop(false);
return count == 1u && gone;
});
passed &= run("a write read inside a later loop under a wider exec keeps its select", [&] {
const auto [count, gone] = laterLoop(true);
return count == 0u && !gone;
});
enum class Latch { Narrowed, Revived };
const auto carried = [](Latch latch, bool readInside) {
Builder b;
auto& wide = b.mask(48u);
auto& exec = b.logicalAnd(wide, b.mask(40u));
auto& old = b.lane();
auto& written = b.select(exec, b.add(old, 1u), old);
const auto [savedLow, savedHigh] = b.ballotWords(exec);
IrBlock& entry = *b.block;
IrBlock& header = b.newBlock();
IrBlock& exit = b.newBlock();
entry.AddBranch(&header);
header.AddBranch(&header);
header.AddBranch(&exit);
b.block = &header;
auto& low = b.phi(IrType::U32, entry, *savedLow);
auto& high = b.phi(IrType::U32, entry, *savedHigh);
auto& value = b.phi(IrType::U32, entry, written);
auto& loopExec = b.threadBit(low, high);
auto& next = b.select(loopExec, b.add(value, 2u), value);
if (readInside) b.keep(b.select(loopExec, b.add(value, 3u), old));
if (latch == Latch::Narrowed) {
const auto [stayLow, stayHigh] = b.ballotWords(b.mask(24u));
low.AddPhiOperand(&header, &b.wordAnd(low, *stayLow));
high.AddPhiOperand(&header, &b.wordAnd(high, *stayHigh));
} else {
const auto [revivedLow, revivedHigh] = b.ballotWords(wide);
low.AddPhiOperand(&header, revivedLow);
high.AddPhiOperand(&header, revivedHigh);
}
value.AddPhiOperand(&header, &next);
b.block = &exit;
b.keep(b.select(b.threadBit(*savedLow, *savedHigh), b.add(value, 4u), old));
return std::pair{b.eliminate(), removed(written)};
};
passed &= run("a write carried through a loop whose exec is saved, narrowed and restored loses its select", [&] {
const auto [count, gone] = carried(Latch::Narrowed, false);
return count == 1u && gone;
});
passed &= run("a write read inside a loop under the loop's narrowed exec loses its select", [&] {
const auto [count, gone] = carried(Latch::Narrowed, true);
return count == 1u && gone;
});
passed &= run("a lane that becomes active again inside a loop and reads the write keeps the select", [&] {
const auto [count, gone] = carried(Latch::Revived, true);
return count == 0u && !gone;
});
passed &= run("a lane that becomes active again inside a loop but never reads the write lets the select go", [&] {
const auto [count, gone] = carried(Latch::Revived, false);
return count == 1u && gone;
});
passed &= run("an exec proven only by assuming a loop phi is forgotten once that phi turns out wider", [] {
Builder b;
auto& wide = b.mask(48u);
auto& exec = b.logicalAnd(wide, b.mask(40u));
auto& old = b.lane();
auto& written = b.select(exec, b.add(old, 1u), old);
const auto [savedLow, savedHigh] = b.ballotWords(exec);
IrBlock& entry = *b.block;
IrBlock& header = b.newBlock();
IrBlock& kill = b.newBlock();
IrBlock& revive = b.newBlock();
IrBlock& latch = b.newBlock();
IrBlock& exit = b.newBlock();
entry.AddBranch(&header);
header.AddBranch(&kill);
header.AddBranch(&revive);
kill.AddBranch(&latch);
revive.AddBranch(&latch);
latch.AddBranch(&header);
latch.AddBranch(&exit);
b.block = &header;
auto& low = b.phi(IrType::U32, entry, *savedLow);
auto& high = b.phi(IrType::U32, entry, *savedHigh);
(void)b.select(b.threadBit(low, high), written, old);
b.block = &kill;
const auto [stayLow, stayHigh] = b.ballotWords(b.mask(24u));
auto& killedLow = b.wordAnd(low, *stayLow);
auto& killedHigh = b.wordAnd(high, *stayHigh);
b.keep(b.select(b.threadBit(killedLow, killedHigh), written, old));
b.block = &revive;
const auto [revivedLow, revivedHigh] = b.ballotWords(wide);
b.block = &latch;
auto& mergedLow = b.phi(IrType::U32, kill, killedLow);
auto& mergedHigh = b.phi(IrType::U32, kill, killedHigh);
mergedLow.AddPhiOperand(&revive, revivedLow);
mergedHigh.AddPhiOperand(&revive, revivedHigh);
low.AddPhiOperand(&latch, &mergedLow);
high.AddPhiOperand(&latch, &mergedHigh);
return b.eliminate() == 0u && !removed(written);
});
passed &= run("a write mask computed inside an earlier loop does not let that loop's phis stand for it", [] {
Builder b;
auto& old = b.lane();
IrBlock& entry = *b.block;
IrBlock& header = b.newBlock();
IrBlock& exit = b.newBlock();
entry.AddBranch(&header);
header.AddBranch(&header);
header.AddBranch(&exit);
b.block = &header;
auto& low = b.phi(IrType::U32, entry, b.constant(0u));
auto& high = b.phi(IrType::U32, entry, b.constant(0u));
auto& bound = b.phi(IrType::U32, entry, b.constant(64u));
auto& exec = b.emit(IrOpcode::ULessThan32, IrType::Bool, {&b.lane(), &bound});
const auto [stayLow, stayHigh] = b.ballotWords(exec);
low.AddPhiOperand(&header, stayLow);
high.AddPhiOperand(&header, stayHigh);
bound.AddPhiOperand(&header, &b.emit(IrOpcode::ISub32, IrType::U32, {&bound, &b.constant(8u)}));
b.block = &exit;
auto& written = b.select(exec, b.add(old, 1u), old);
b.keep(b.select(b.threadBit(low, high), b.add(written, 2u), old));
return b.eliminate() == 0u && !removed(written);
});
const auto wholeQuads = [](Builder& b, IrValue& live) -> IrValue& {
auto& ballot = b.emit(IrOpcode::Ballot, IrType::U32x4, {&live});
auto& allLow = b.emit(IrOpcode::Ballot, IrType::U32x4, {&b.truth()});
auto& allHigh = b.emit(IrOpcode::Ballot, IrType::U32x4, {&b.truth()});
auto& low = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&ballot, &b.constant(0u)}), &b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&allLow, &b.constant(0u)})});
auto& high = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&ballot, &b.constant(1u)}), &b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&allHigh, &b.constant(1u)})});
auto& wqm = b.emit(IrOpcode::WqmU64, IrType::U64, {&b.emit(IrOpcode::CompositeConstructU64, IrType::U64, {&low, &high})});
return b.threadBit(b.emit(IrOpcode::CompositeExtractU64, IrType::U32, {&wqm, &b.constant(0u)}), b.emit(IrOpcode::CompositeExtractU64, IrType::U32, {&wqm, &b.constant(1u)}));
};
const auto sample = [&](IrShaderStage stage, std::uint32_t sampleFlags, IrOpcode opcode = IrOpcode::ImageSampleRaw, bool wholeQuadExec = false) {
Builder b;
b.program.Resources().stage = stage;
MemoryInfo memory;
memory.kind = ResourceKind::Image;
memory.imageSampleFlags = sampleFlags;
b.program.Resources().memoryInfo.push_back(memory);
auto& exec = wholeQuadExec ? wholeQuads(b, b.mask(16u)) : b.mask(16u);
auto& old = b.lane();
auto& written = b.select(exec, b.add(old, 1u), old);
auto& address = b.emit(IrOpcode::MakeImageAddress, IrType::ImageAddress, {&written, &old});
auto& image = b.emit(IrOpcode::GetImageResource, IrType::ImageResource, {});
auto& sampler = b.emit(IrOpcode::GetSamplerResource, IrType::SamplerResource, {});
MemoryFlags flags{0u, 0u};
std::uint64_t bits = 0;
std::memcpy(&bits, &flags, sizeof(flags));
auto& sampled = b.emit(opcode, IrType::U32x4, {&image, &sampler, &address}, bits);
b.keep(b.select(exec, b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&sampled, &b.constant(0u)}), old));
b.eliminate();
return removed(written);
};
passed &= run("an implicit-LOD pixel sample reads its quad's coordinates and keeps the select", [&] {
return !sample(IrShaderStage::Pixel, 0u);
});
passed &= run("an implicit-LOD pixel sample under whole quad mode reads no masked quad and drops the select", [&] {
return sample(IrShaderStage::Pixel, 0u, IrOpcode::ImageSampleRaw, true);
});
passed &= run("an explicit-LOD sample reads only its lane and drops the select", [&] {
return sample(IrShaderStage::Pixel, RdnaImageSampleFlagLevelZero) && sample(IrShaderStage::Mesh, 0u);
});
passed &= run("a pixel gather without an LOD of its own takes it from the quad and keeps the select", [&] {
return !sample(IrShaderStage::Pixel, 0u, IrOpcode::ImageGatherRaw) && !sample(IrShaderStage::Pixel, RdnaImageSampleFlagBias, IrOpcode::ImageGatherRaw);
});
passed &= run("a gather with an explicit LOD reads only its lane and drops the select", [&] {
return sample(IrShaderStage::Pixel, RdnaImageSampleFlagLevelZero, IrOpcode::ImageGatherRaw) && sample(IrShaderStage::Pixel, RdnaImageSampleFlagLod, IrOpcode::ImageGatherRaw) &&
sample(IrShaderStage::Mesh, 0u, IrOpcode::ImageGatherRaw);
});
passed &= run("an image size query reads only its lane's level and drops the select", [] {
Builder b;
auto& exec = b.mask(16u);
auto& old = b.lane();
auto& written = b.select(exec, b.add(old, 1u), old);
auto& address = b.emit(IrOpcode::MakeImageAddress, IrType::ImageAddress, {&written});
auto& size = b.emit(IrOpcode::ImageQueryDimensions, IrType::U32x4, {&b.emit(IrOpcode::GetImageResource, IrType::ImageResource, {}), &address});
b.keep(b.select(exec, b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&size, &b.constant(0u)}), old));
return b.eliminate() == 1u && removed(written);
});
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;
case IrOpcode::ImageRead: {
auto& address = b.emit(IrOpcode::MakeImageAddress, IrType::ImageAddress, {&written, &old});
auto& image = b.emit(IrOpcode::GetImageResource, IrType::ImageResource, {});
auto& read = b.emit(opcode, IrType::U32x4, {&image, &address, &active});
b.keep(b.select(exec, b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&read, &b.constant(0u)}), old));
break;
}
case IrOpcode::ImageWrite: {
auto& address = b.emit(IrOpcode::MakeImageAddress, IrType::ImageAddress, {&old, &old});
auto& texel = b.emit(IrOpcode::CompositeConstructU32x4, IrType::U32x4, {&written, &old, &old, &old});
b.emit(opcode, IrType::Void, {&b.emit(IrOpcode::GetImageResource, IrType::ImageResource, {}), &address, &texel, &active});
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::ImageRead, 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::ImageRead, false, false) && !guarded(IrOpcode::SetAttribute, false, false);
});
passed &= run("an image write under the write's exec drops the select, under a wider exec or as its exec keeps it", [&] {
return guarded(IrOpcode::ImageWrite, true, false) && !guarded(IrOpcode::ImageWrite, false, false) && !guarded(IrOpcode::ImageWrite, true, true);
});
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);
});
passed &= run("a write under whole quad mode read only by the lanes it was widened from drops the select in every stage", [&] {
const auto widened = [&](IrShaderStage stage) {
Builder b;
b.program.Resources().stage = stage;
auto& live = b.mask(16u);
auto& quads = wholeQuads(b, live);
auto& old = b.lane();
auto& written = b.select(quads, b.add(old, 1u), old);
auto& next = b.select(b.logicalAnd(live, b.mask(8u)), b.add(written, 2u), old);
b.keep(next);
return b.eliminate() != 0u && removed(written) && !removed(next);
};
return widened(IrShaderStage::Pixel) && widened(IrShaderStage::Compute);
});
passed &= run("a write under whole quad mode read by a lane outside the widened mask keeps the select in compute", [&] {
Builder b;
b.program.Resources().stage = IrShaderStage::Compute;
auto& live = b.mask(16u);
auto& quads = wholeQuads(b, live);
auto& old = b.lane();
auto& written = b.select(quads, b.add(old, 1u), old);
auto& next = b.select(b.mask(32u), b.add(written, 2u), old);
b.keep(next);
return b.eliminate() == 0u && !removed(written);
});
passed &= run("a write under whole quad mode read only by the lanes it was widened from drops the select", [&] {
Builder b;
auto& live = b.mask(16u);
auto& quads = wholeQuads(b, live);
auto& old = b.lane();
auto& written = b.select(quads, b.add(old, 1u), old);
auto& next = b.select(b.logicalAnd(live, b.mask(8u)), b.add(written, 2u), old);
b.keep(next);
return b.eliminate() != 0u && removed(written) && !removed(next);
});
passed &= run("a write under whole quad mode read by other lanes keeps the select", [&] {
Builder b;
auto& live = b.mask(16u);
auto& quads = wholeQuads(b, live);
auto& old = b.lane();
auto& written = b.select(quads, b.add(old, 1u), old);
auto& next = b.select(b.mask(32u), b.add(written, 2u), old);
b.keep(next);
return b.eliminate() == 0u && !removed(written);
});
const auto shaded = [](std::uint32_t units) {
Builder b;
b.program.Resources().stage = IrShaderStage::Pixel;
MemoryInfo memory;
memory.kind = ResourceKind::Image;
memory.imageSampleFlags = RdnaImageSampleFlagLevelZero;
b.program.Resources().memoryInfo.push_back(memory);
MemoryFlags flags{0u, 0u};
std::uint64_t bits = 0;
std::memcpy(&bits, &flags, sizeof(flags));
auto& exec = b.mask(48u);
auto& image = b.emit(IrOpcode::GetImageResource, IrType::ImageResource, {});
auto& sampler = b.emit(IrOpcode::GetSamplerResource, IrType::SamplerResource, {});
IrValue* color = &b.lane();
IrValue* address = nullptr;
std::uint32_t kept = 0;
std::vector<IrValue*> compared;
for (std::uint32_t unit = 0; unit < units; ++unit) {
auto& sum = b.add(*color, unit);
auto& low = b.emit(IrOpcode::ULessThan32, IrType::Bool, {&sum, &b.constant(100u + unit)});
compared.push_back(&b.select(low, sum, b.add(*color, 1u)));
address = &b.emit(IrOpcode::MakeImageAddress, IrType::ImageAddress, {compared.back(), color});
auto& gathered = b.emit(IrOpcode::ImageGatherRaw, IrType::U32x4, {&image, &sampler, address}, bits);
color = &b.select(exec, b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&gathered, &b.constant(0u)}), *color);
}
b.emit(IrOpcode::ImageWrite, IrType::Void, {&image, address, &b.emit(IrOpcode::CompositeConstructU32x4, IrType::U32x4, {color, color, color, color}), &exec});
const auto stats = MaskedSelectEliminator{}.Eliminate(b.program);
for (const IrValue* select : compared) kept += removed(*select) ? 0u : 1u;
return std::pair{stats, kept};
};
passed &= run("selects that each reach the rest of the program are settled in steps that grow with the program, not with its square", [&] {
const auto [small, smallKept] = shaded(500u);
const auto [large, largeKept] = shaded(4000u);
return small.removedSelects == 500u && smallKept == 500u && large.removedSelects == 4000u && largeKept == 4000u && small.steps != 0u && large.steps <= 9u * small.steps;
});
return passed ? 0 : 1;
}