396 lines
22 KiB
C++
396 lines
22 KiB
C++
#include "ControlFlow/Structurizer.hpp"
|
|
#include <algorithm>
|
|
#include <cstdio>
|
|
#include <exception>
|
|
#include <iterator>
|
|
#include <map>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
using namespace ShaderRecompiler;
|
|
|
|
namespace {
|
|
|
|
ControlFlowGraph makeGraph(const std::vector<std::vector<std::uint32_t>>& successors) {
|
|
ControlFlowGraph graph;
|
|
graph.entryBlock = 0;
|
|
for (std::uint32_t id = 0; id < successors.size(); ++id) {
|
|
BasicBlock block;
|
|
block.id = id;
|
|
block.startProgramCounter = id * 8;
|
|
block.endProgramCounter = id * 8 + 8;
|
|
block.instructionBegin = id * 2;
|
|
block.instructionEnd = id * 2 + 2;
|
|
block.successors = successors[id];
|
|
auto& terminator = block.terminator;
|
|
if (successors[id].empty()) {
|
|
terminator.kind = TerminatorKind::Return;
|
|
} else if (successors[id].size() == 1) {
|
|
terminator.kind = TerminatorKind::Branch;
|
|
terminator.trueBlock = successors[id][0];
|
|
} else {
|
|
terminator.kind = TerminatorKind::ConditionalBranch;
|
|
terminator.condition = BranchCondition::SccNonZero;
|
|
terminator.trueBlock = successors[id][0];
|
|
terminator.falseBlock = successors[id][1];
|
|
}
|
|
graph.blocks.push_back(std::move(block));
|
|
}
|
|
for (const auto& block : graph.blocks) {
|
|
for (const auto successor : block.successors) graph.blocks[successor].predecessors.push_back(block.id);
|
|
}
|
|
return graph;
|
|
}
|
|
|
|
const BasicBlock* innermostLoopHeader(const ControlFlowGraph& graph, std::uint32_t blockId) {
|
|
const BasicBlock* innermost = nullptr;
|
|
std::size_t innermostSize = 0;
|
|
for (const auto& header : graph.blocks) {
|
|
if (!header.terminator.loopHeader || !graph.Dominates(header.id, blockId) || graph.Dominates(header.terminator.mergeBlock, blockId) || blockId == header.terminator.mergeBlock) continue;
|
|
const auto size = static_cast<std::size_t>(std::count_if(graph.blocks.begin(), graph.blocks.end(), [&](const BasicBlock& block) { return graph.Dominates(header.id, block.id) && !graph.Dominates(header.terminator.mergeBlock, block.id); }));
|
|
if (innermost == nullptr || size < innermostSize) {
|
|
innermost = &header;
|
|
innermostSize = size;
|
|
}
|
|
}
|
|
return innermost;
|
|
}
|
|
|
|
void requireStructuredBranches(const ControlFlowGraph& graph, const char* name) {
|
|
for (const auto& block : graph.blocks) {
|
|
const auto& terminator = block.terminator;
|
|
if (terminator.kind != TerminatorKind::ConditionalBranch || terminator.loopHeader || terminator.trueBlock == terminator.falseBlock) continue;
|
|
const auto* loop = innermostLoopHeader(graph, block.id);
|
|
if (terminator.mergeBlock != InvalidControlFlowId) {
|
|
if (loop != nullptr && terminator.mergeBlock == loop->terminator.continueBlock) {
|
|
throw std::runtime_error(std::string(name) + ": block " + std::to_string(block.id) + " in the loop at block " + std::to_string(loop->id) + " merges at the loop's continue block " + std::to_string(terminator.mergeBlock));
|
|
}
|
|
continue;
|
|
}
|
|
const auto exits = [&](std::uint32_t target) { return loop != nullptr && (target == loop->terminator.mergeBlock || target == loop->terminator.continueBlock); };
|
|
if (!exits(terminator.trueBlock) && !exits(terminator.falseBlock)) {
|
|
throw std::runtime_error(std::string(name) + ": block " + std::to_string(block.id) + " branches to " + std::to_string(terminator.trueBlock) + "/" + std::to_string(terminator.falseBlock) + " without a merge, and neither is its loop's merge or continue");
|
|
}
|
|
}
|
|
}
|
|
|
|
void requireExactPostDominators(const ControlFlowGraph& graph, const char* name) {
|
|
const auto count = static_cast<std::uint32_t>(graph.blocks.size());
|
|
const auto reachesExitAvoiding = [&](std::uint32_t from, std::uint32_t avoided) {
|
|
std::vector<bool> seen(count, false);
|
|
std::vector<std::uint32_t> stack{from};
|
|
seen[from] = true;
|
|
while (!stack.empty()) {
|
|
const auto id = stack.back();
|
|
stack.pop_back();
|
|
if (graph.blocks[id].successors.empty()) return true;
|
|
for (const auto successor : graph.blocks[id].successors) {
|
|
if (successor == avoided || seen[successor]) continue;
|
|
seen[successor] = true;
|
|
stack.push_back(successor);
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
for (const auto& block : graph.blocks) {
|
|
std::vector<std::uint32_t> expected;
|
|
for (std::uint32_t candidate = 0; candidate < count; ++candidate) {
|
|
if (candidate == block.id || !reachesExitAvoiding(block.id, candidate)) expected.push_back(candidate);
|
|
}
|
|
if (block.postDominators != expected) {
|
|
throw std::runtime_error(std::string(name) + ": block " + std::to_string(block.id) + " has " + std::to_string(block.postDominators.size()) + " post-dominators, expected " + std::to_string(expected.size()));
|
|
}
|
|
}
|
|
}
|
|
|
|
void requireDominatedMerges(const ControlFlowGraph& graph, const char* name) {
|
|
for (const auto& block : graph.blocks) {
|
|
const auto merge = block.terminator.mergeBlock;
|
|
if (merge != InvalidControlFlowId && !graph.Dominates(block.id, merge)) {
|
|
throw std::runtime_error(std::string(name) + ": header " + std::to_string(block.id) + " does not dominate its merge " + std::to_string(merge));
|
|
}
|
|
}
|
|
}
|
|
|
|
using Routes = std::map<std::uint32_t, bool>;
|
|
|
|
std::uint32_t originalBlock(const ControlFlowGraph& graph, std::uint32_t blockId, Routes& routes) {
|
|
for (std::size_t step = 0; step <= graph.blocks.size(); ++step) {
|
|
const auto& block = graph.FindBlock(blockId);
|
|
const auto& terminator = block.terminator;
|
|
if (block.instructionBegin != block.instructionEnd) return blockId;
|
|
if (terminator.gotoValue >= 0) routes[terminator.gotoVariable] = terminator.gotoValue != 0;
|
|
if (terminator.kind == TerminatorKind::Branch) {
|
|
blockId = terminator.trueBlock;
|
|
} else if (terminator.kind == TerminatorKind::ConditionalBranch && terminator.condition == BranchCondition::GotoVariable && routes.contains(terminator.gotoVariable)) {
|
|
blockId = routes.at(terminator.gotoVariable) ? terminator.trueBlock : terminator.falseBlock;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
throw std::runtime_error("block " + std::to_string(blockId) + " does not lead to an original block");
|
|
}
|
|
|
|
void requireOriginalBranches(const ControlFlowGraph& graph, const std::vector<std::vector<std::uint32_t>>& successors, const char* name) {
|
|
std::vector<bool> seen(successors.size(), false);
|
|
Routes entryRoutes;
|
|
std::vector<std::pair<std::uint32_t, Routes>> pending{{originalBlock(graph, graph.entryBlock, entryRoutes), entryRoutes}};
|
|
std::vector<std::pair<std::uint32_t, Routes>> visited;
|
|
while (!pending.empty()) {
|
|
auto state = std::move(pending.back());
|
|
pending.pop_back();
|
|
if (std::find(visited.begin(), visited.end(), state) != visited.end()) continue;
|
|
visited.push_back(state);
|
|
const auto& block = graph.FindBlock(state.first);
|
|
const auto original = block.instructionBegin / 2;
|
|
std::vector<std::uint32_t> branchTargets;
|
|
if (block.terminator.kind == TerminatorKind::Branch) branchTargets = {block.terminator.trueBlock};
|
|
if (block.terminator.kind == TerminatorKind::ConditionalBranch) branchTargets = {block.terminator.trueBlock, block.terminator.falseBlock};
|
|
std::vector<std::uint32_t> targets;
|
|
for (const auto target : branchTargets) {
|
|
auto routes = state.second;
|
|
const auto reached = originalBlock(graph, target, routes);
|
|
targets.push_back(graph.FindBlock(reached).instructionBegin / 2);
|
|
pending.emplace_back(reached, std::move(routes));
|
|
}
|
|
if (targets != successors[original]) {
|
|
throw std::runtime_error(std::string(name) + ": block " + std::to_string(original) + " no longer branches to its original targets");
|
|
}
|
|
seen[original] = true;
|
|
}
|
|
if (std::find(seen.begin(), seen.end(), false) != seen.end()) {
|
|
throw std::runtime_error(std::string(name) + ": an original block is missing");
|
|
}
|
|
}
|
|
|
|
void requireStructuredLoop(const std::vector<std::vector<std::uint32_t>>& successors, const char* name) {
|
|
auto graph = makeGraph(successors);
|
|
Structurizer{}.Structurize(graph);
|
|
requireExactPostDominators(graph, name);
|
|
requireStructuredBranches(graph, name);
|
|
requireDominatedMerges(graph, name);
|
|
requireOriginalBranches(graph, successors, name);
|
|
}
|
|
|
|
std::vector<std::vector<std::uint32_t>> originalPostDominators(const ControlFlowGraph& graph) {
|
|
std::vector<std::uint32_t> all;
|
|
for (std::uint32_t id = 0; id < graph.blocks.size(); ++id) all.push_back(id);
|
|
std::vector<std::vector<std::uint32_t>> result;
|
|
for (const auto& block : graph.blocks) result.push_back(block.successors.empty() ? std::vector<std::uint32_t>{block.id} : all);
|
|
bool changed = true;
|
|
while (changed) {
|
|
changed = false;
|
|
for (const auto& block : graph.blocks) {
|
|
std::vector<std::uint32_t> next;
|
|
if (block.successors.empty()) {
|
|
next = {block.id};
|
|
} else {
|
|
next = result[block.successors.front()];
|
|
for (std::size_t index = 1; index < block.successors.size(); ++index) {
|
|
std::vector<std::uint32_t> intersection;
|
|
const auto& successor = result[block.successors[index]];
|
|
std::set_intersection(next.begin(), next.end(), successor.begin(), successor.end(), std::back_inserter(intersection));
|
|
next = std::move(intersection);
|
|
}
|
|
if (std::find(next.begin(), next.end(), block.id) == next.end()) next.push_back(block.id);
|
|
std::sort(next.begin(), next.end());
|
|
next.erase(std::unique(next.begin(), next.end()), next.end());
|
|
}
|
|
if (next != result[block.id]) {
|
|
result[block.id] = std::move(next);
|
|
changed = true;
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::vector<std::vector<std::uint32_t>> originalDominators(const ControlFlowGraph& graph) {
|
|
std::vector<std::uint32_t> all;
|
|
for (std::uint32_t id = 0; id < graph.blocks.size(); ++id) all.push_back(id);
|
|
std::vector<std::vector<std::uint32_t>> result;
|
|
for (const auto& block : graph.blocks) result.push_back(block.id == graph.entryBlock ? std::vector<std::uint32_t>{block.id} : all);
|
|
bool changed = true;
|
|
while (changed) {
|
|
changed = false;
|
|
for (const auto& block : graph.blocks) {
|
|
if (block.id == graph.entryBlock) continue;
|
|
std::vector<std::uint32_t> next;
|
|
if (block.predecessors.empty()) {
|
|
next = {block.id};
|
|
} else {
|
|
next = result[block.predecessors.front()];
|
|
for (std::size_t index = 1; index < block.predecessors.size(); ++index) {
|
|
std::vector<std::uint32_t> intersection;
|
|
const auto& predecessor = result[block.predecessors[index]];
|
|
std::set_intersection(next.begin(), next.end(), predecessor.begin(), predecessor.end(), std::back_inserter(intersection));
|
|
next = std::move(intersection);
|
|
}
|
|
if (std::find(next.begin(), next.end(), block.id) == next.end()) next.push_back(block.id);
|
|
std::sort(next.begin(), next.end());
|
|
next.erase(std::unique(next.begin(), next.end()), next.end());
|
|
}
|
|
if (next != result[block.id]) {
|
|
result[block.id] = std::move(next);
|
|
changed = true;
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void requireOriginalDominanceSets(ControlFlowGraph graph, const char* name) {
|
|
for (auto& block : graph.blocks) {
|
|
block.dominators.clear();
|
|
block.postDominators.clear();
|
|
}
|
|
const auto expected = originalPostDominators(graph);
|
|
const auto expectedDominators = originalDominators(graph);
|
|
const auto original = graph;
|
|
const std::string stopAfterAnalyses = "dominance differential fixture: stop before CFG rewrites";
|
|
graph.unsupported = true;
|
|
graph.unsupportedReason = stopAfterAnalyses;
|
|
bool stopped = false;
|
|
try {
|
|
Structurizer{}.Structurize(graph);
|
|
} catch (const std::runtime_error& error) {
|
|
if (error.what() != stopAfterAnalyses) throw;
|
|
stopped = true;
|
|
}
|
|
if (!stopped || graph.blocks.size() != original.blocks.size() || graph.entryBlock != original.entryBlock) {
|
|
throw std::runtime_error(std::string(name) + ": differential fixture did not stop before CFG rewrites");
|
|
}
|
|
for (std::size_t id = 0; id < graph.blocks.size(); ++id) {
|
|
const auto& block = graph.blocks[id];
|
|
if (block.id != original.blocks[id].id || block.successors != original.blocks[id].successors || block.predecessors != original.blocks[id].predecessors) {
|
|
throw std::runtime_error(std::string(name) + ": differential fixture changed the CFG");
|
|
}
|
|
if (block.postDominators != expected[id]) {
|
|
throw std::runtime_error(std::string(name) + ": block " + std::to_string(id) + " differs from the original post-dominator fixed point");
|
|
}
|
|
if (block.dominators != expectedDominators[id]) {
|
|
throw std::runtime_error(std::string(name) + ": block " + std::to_string(id) + " differs from the original dominator fixed point");
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
int main() {
|
|
try {
|
|
auto largeBlocks = makeGraph({{1}, {2, 3}, {1, 4}, {5, 6}, {1, 7}, {8, 7}, {9, 4}, {10}, {6, 10}, {10}, {}});
|
|
for (auto& block : largeBlocks.blocks) block.estimatedSpirvWords = 1000u;
|
|
Structurizer{}.Structurize(largeBlocks);
|
|
requireStructuredBranches(largeBlocks, "large loop exit blocks retain the baseline result");
|
|
auto exitJunction = makeGraph({{1}, {2, 3}, {9}, {4, 6}, {5, 9}, {1}, {7, 8}, {9}, {1}, {}});
|
|
Structurizer{}.Structurize(exitJunction);
|
|
requireStructuredBranches(exitJunction, "shared loop exit junctions retain structured branches");
|
|
auto nested = makeGraph({{1}, {2}, {5, 3}, {5, 4}, {7}, {6, 7}, {}, {1}});
|
|
Structurizer{}.Structurize(nested);
|
|
requireExactPostDominators(nested, "nested selections");
|
|
if (nested.FindBlock(2).terminator.mergeBlock == nested.FindBlock(3).terminator.mergeBlock) {
|
|
std::fprintf(stderr, "the nested selections share merge block %u\n", nested.FindBlock(2).terminator.mergeBlock);
|
|
return 1;
|
|
}
|
|
requireStructuredBranches(nested, "nested selections");
|
|
auto exitTail = makeGraph({{1}, {2}, {3, 4}, {6}, {6, 5}, {1}, {}});
|
|
Structurizer{}.Structurize(exitTail);
|
|
requireExactPostDominators(exitTail, "a loop exit through a tail block");
|
|
requireStructuredBranches(exitTail, "a loop exit through a tail block");
|
|
auto exitTails = makeGraph({{1}, {2}, {3, 4}, {7}, {5, 6}, {7, 1}, {7}, {}});
|
|
Structurizer{}.Structurize(exitTails);
|
|
requireExactPostDominators(exitTails, "a loop with two exit tails");
|
|
requireStructuredBranches(exitTails, "a loop with two exit tails");
|
|
auto endingExits = makeGraph({{6, 1}, {2}, {3}, {6, 4}, {2, 5}, {}, {}});
|
|
Structurizer{}.Structurize(endingExits);
|
|
requireExactPostDominators(endingExits, "a loop whose two exits end the program");
|
|
requireStructuredBranches(endingExits, "a loop whose two exits end the program");
|
|
auto threeEndingExits = makeGraph({{1}, {2}, {5, 3}, {6, 4}, {1, 7}, {}, {}, {}});
|
|
Structurizer{}.Structurize(threeEndingExits);
|
|
requireExactPostDominators(threeEndingExits, "a loop whose three exits end the program");
|
|
requireStructuredBranches(threeEndingExits, "a loop whose three exits end the program");
|
|
auto innerEndingExit = makeGraph({{1}, {2}, {3}, {7, 4}, {2, 5}, {1, 6}, {}, {}});
|
|
Structurizer{}.Structurize(innerEndingExit);
|
|
requireExactPostDominators(innerEndingExit, "an inner loop exit that ends the program");
|
|
requireStructuredBranches(innerEndingExit, "an inner loop exit that ends the program");
|
|
requireStructuredLoop({{1}, {2, 4}, {3, 4}, {4, 5}, {1, 6}, {1}, {}}, "nested selections in a loop with two latches");
|
|
requireStructuredLoop({{1}, {5, 2}, {5, 3}, {6, 4}, {7, 5}, {8, 1}, {1}, {1}, {}}, "nested selections on the false side in a loop with three latches");
|
|
requireStructuredLoop({{1}, {2, 5}, {3, 4}, {5, 6}, {5}, {1, 7}, {1}, {}}, "an if-else in a loop whose one arm continues");
|
|
requireStructuredLoop({{1}, {2}, {3, 4}, {2, 5}, {}, {1}}, "an inner loop that returns or continues its outer loop");
|
|
requireStructuredLoop({{1}, {2, 3}, {4, 5}, {}, {3, 6}, {1}, {2}}, "an inner loop that leaves both loops or continues its outer loop");
|
|
requireStructuredLoop({{1, 3}, {2, 4}, {3, 1}, {4}, {}}, "a loop whose latch exits into a block the branch before the loop also reaches");
|
|
requireStructuredLoop({{1, 2}, {3, 2}, {4}, {4, 1}, {}}, "a loop whose header exits into a block the branch before the loop also reaches");
|
|
requireStructuredLoop({{1, 2}, {4}, {3, 1}, {4, 2}, {}}, "a loop whose header exits into a block the branch before the loop reaches first");
|
|
auto earlyReturn = makeGraph({{2, 1}, {4, 2}, {3, 5}, {5}, {}, {}});
|
|
Structurizer{}.Structurize(earlyReturn);
|
|
requireExactPostDominators(earlyReturn, "an early return inside a selection that joins its parent's merge");
|
|
requireStructuredBranches(earlyReturn, "an early return inside a selection that joins its parent's merge");
|
|
requireDominatedMerges(earlyReturn, "an early return inside a selection that joins its parent's merge");
|
|
std::vector<std::vector<std::uint32_t>> diamonds;
|
|
for (std::uint32_t diamond = 0; diamond < 50u; ++diamond) {
|
|
const auto head = diamond * 3u;
|
|
diamonds.push_back({head + 1u, head + 2u});
|
|
diamonds.push_back({head + 3u});
|
|
diamonds.push_back({head + 3u});
|
|
}
|
|
diamonds.push_back({});
|
|
auto chain = makeGraph(diamonds);
|
|
Structurizer{}.Structurize(chain);
|
|
requireExactPostDominators(chain, "a chain of fifty selections");
|
|
requireStructuredLoop({{1}, {2, 8}, {3, 5}, {4, 8}, {7}, {6, 7}, {7, 8}, {1}, {}}, "nested selections sharing a loop continuation");
|
|
auto sharedDiscard = makeGraph({{5, 1}, {4, 2}, {7, 3}, {7, 4}, {5}, {7, 6}, {}, {}});
|
|
const std::vector<std::uint32_t> sharedDiscardWords{444, 1944, 68, 1128, 8, 2084, 148, 28};
|
|
for (auto& block : sharedDiscard.blocks) {
|
|
std::sort(block.successors.begin(), block.successors.end());
|
|
block.estimatedSpirvWords = sharedDiscardWords[block.id];
|
|
}
|
|
Structurizer{}.Structurize(sharedDiscard);
|
|
requireStructuredBranches(sharedDiscard, "early discards that share one ending block");
|
|
requireOriginalDominanceSets(makeGraph(diamonds), "raw diamond chain");
|
|
requireOriginalDominanceSets(makeGraph({{1}, {2, 3}, {1}, {}}), "a loop with an exit");
|
|
requireOriginalDominanceSets(makeGraph({{1}, {2}, {1}}), "a non-exiting SCC");
|
|
requireOriginalDominanceSets(makeGraph({{1, 3}, {2}, {1}, {}}), "an exit-or-spin branch");
|
|
requireOriginalDominanceSets(makeGraph({{1}, {}, {3}, {}, {5}, {4}}), "unreachable exit and non-exiting islands");
|
|
auto reordered = makeGraph({{1}, {}, {2}, {0, 4}, {1}});
|
|
reordered.entryBlock = 3;
|
|
requireOriginalDominanceSets(std::move(reordered), "a nonzero entry with permuted block IDs");
|
|
requireOriginalDominanceSets(makeGraph({{1, 2}, {}, {}}), "disjoint exits with an empty post-dominator intersection");
|
|
requireOriginalDominanceSets(makeGraph({{2}, {2}, {}}), "separate roots with an empty dominator intersection");
|
|
requireOriginalDominanceSets(makeGraph({{1}, {1, 2}, {}}), "a self-loop with an exit");
|
|
std::uint32_t randomState = 0x7b19a5c3u;
|
|
const auto randomWord = [&] { randomState = randomState * 1664525u + 1013904223u; return randomState; };
|
|
for (std::uint32_t trial = 0; trial < 24u; ++trial) {
|
|
const auto count = 8u + trial % 9u;
|
|
std::vector<std::uint32_t> ids(count);
|
|
for (std::uint32_t id = 0; id < count; ++id) ids[id] = id;
|
|
for (auto size = count; size > 1u; --size) std::swap(ids[size - 1u], ids[randomWord() % size]);
|
|
if (ids.front() == 0u) std::swap(ids.front(), ids.back());
|
|
std::vector<std::vector<std::uint32_t>> successors(count);
|
|
for (std::uint32_t position = 0; position + 1u < count; ++position) {
|
|
const auto remaining = count - position - 1u;
|
|
const auto degree = position == 0u ? 2u : position == 1u ? 1u : std::min(randomWord() % 3u, remaining);
|
|
if (degree == 0u) continue;
|
|
auto& edges = successors[ids[position]];
|
|
const auto first = position + 1u + randomWord() % remaining;
|
|
edges.push_back(ids[first]);
|
|
if (degree == 2u) {
|
|
auto second = position + 1u + randomWord() % (remaining - 1u);
|
|
if (second >= first) ++second;
|
|
edges.push_back(ids[second]);
|
|
}
|
|
std::sort(edges.begin(), edges.end());
|
|
}
|
|
auto dag = makeGraph(successors);
|
|
dag.entryBlock = ids.front();
|
|
const auto name = "deterministic DAG " + std::to_string(trial);
|
|
requireOriginalDominanceSets(std::move(dag), name.c_str());
|
|
}
|
|
} catch (const std::exception& error) {
|
|
std::fprintf(stderr, "%s\n", error.what());
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|