#include "IntermediateRepresentation/IrProgram.hpp" #include "Optimization/MaskedSelectEliminator.hpp" #include "RdnaDecoder/RdnaInstruction.hpp" #include #include #include #include #include #include #include #include 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 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 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& 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 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; }