summaryrefslogtreecommitdiff
path: root/src/shader_recompiler/frontend/maxwell/control_flow.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/shader_recompiler/frontend/maxwell/control_flow.cpp')
-rw-r--r--src/shader_recompiler/frontend/maxwell/control_flow.cpp642
1 files changed, 642 insertions, 0 deletions
diff --git a/src/shader_recompiler/frontend/maxwell/control_flow.cpp b/src/shader_recompiler/frontend/maxwell/control_flow.cpp
new file mode 100644
index 000000000..1a954a509
--- /dev/null
+++ b/src/shader_recompiler/frontend/maxwell/control_flow.cpp
@@ -0,0 +1,642 @@
1// Copyright 2021 yuzu Emulator Project
2// Licensed under GPLv2 or any later version
3// Refer to the license.txt file included.
4
5#include <algorithm>
6#include <array>
7#include <optional>
8#include <string>
9#include <utility>
10
11#include <fmt/format.h>
12
13#include "shader_recompiler/exception.h"
14#include "shader_recompiler/frontend/maxwell/control_flow.h"
15#include "shader_recompiler/frontend/maxwell/decode.h"
16#include "shader_recompiler/frontend/maxwell/indirect_branch_table_track.h"
17#include "shader_recompiler/frontend/maxwell/location.h"
18
19namespace Shader::Maxwell::Flow {
20namespace {
21struct Compare {
22 bool operator()(const Block& lhs, Location rhs) const noexcept {
23 return lhs.begin < rhs;
24 }
25
26 bool operator()(Location lhs, const Block& rhs) const noexcept {
27 return lhs < rhs.begin;
28 }
29
30 bool operator()(const Block& lhs, const Block& rhs) const noexcept {
31 return lhs.begin < rhs.begin;
32 }
33};
34
35u32 BranchOffset(Location pc, Instruction inst) {
36 return pc.Offset() + static_cast<u32>(inst.branch.Offset()) + 8u;
37}
38
39void Split(Block* old_block, Block* new_block, Location pc) {
40 if (pc <= old_block->begin || pc >= old_block->end) {
41 throw InvalidArgument("Invalid address to split={}", pc);
42 }
43 *new_block = Block{};
44 new_block->begin = pc;
45 new_block->end = old_block->end;
46 new_block->end_class = old_block->end_class;
47 new_block->cond = old_block->cond;
48 new_block->stack = old_block->stack;
49 new_block->branch_true = old_block->branch_true;
50 new_block->branch_false = old_block->branch_false;
51 new_block->function_call = old_block->function_call;
52 new_block->return_block = old_block->return_block;
53 new_block->branch_reg = old_block->branch_reg;
54 new_block->branch_offset = old_block->branch_offset;
55 new_block->indirect_branches = std::move(old_block->indirect_branches);
56
57 const Location old_begin{old_block->begin};
58 Stack old_stack{std::move(old_block->stack)};
59 *old_block = Block{};
60 old_block->begin = old_begin;
61 old_block->end = pc;
62 old_block->end_class = EndClass::Branch;
63 old_block->cond = IR::Condition(true);
64 old_block->stack = old_stack;
65 old_block->branch_true = new_block;
66 old_block->branch_false = nullptr;
67}
68
69Token OpcodeToken(Opcode opcode) {
70 switch (opcode) {
71 case Opcode::PBK:
72 case Opcode::BRK:
73 return Token::PBK;
74 case Opcode::PCNT:
75 case Opcode::CONT:
76 return Token::PBK;
77 case Opcode::PEXIT:
78 case Opcode::EXIT:
79 return Token::PEXIT;
80 case Opcode::PLONGJMP:
81 case Opcode::LONGJMP:
82 return Token::PLONGJMP;
83 case Opcode::PRET:
84 case Opcode::RET:
85 case Opcode::CAL:
86 return Token::PRET;
87 case Opcode::SSY:
88 case Opcode::SYNC:
89 return Token::SSY;
90 default:
91 throw InvalidArgument("{}", opcode);
92 }
93}
94
95bool IsAbsoluteJump(Opcode opcode) {
96 switch (opcode) {
97 case Opcode::JCAL:
98 case Opcode::JMP:
99 case Opcode::JMX:
100 return true;
101 default:
102 return false;
103 }
104}
105
106bool HasFlowTest(Opcode opcode) {
107 switch (opcode) {
108 case Opcode::BRA:
109 case Opcode::BRX:
110 case Opcode::EXIT:
111 case Opcode::JMP:
112 case Opcode::JMX:
113 case Opcode::KIL:
114 case Opcode::BRK:
115 case Opcode::CONT:
116 case Opcode::LONGJMP:
117 case Opcode::RET:
118 case Opcode::SYNC:
119 return true;
120 case Opcode::CAL:
121 case Opcode::JCAL:
122 return false;
123 default:
124 throw InvalidArgument("Invalid branch {}", opcode);
125 }
126}
127
128std::string NameOf(const Block& block) {
129 if (block.begin.IsVirtual()) {
130 return fmt::format("\"Virtual {}\"", block.begin);
131 } else {
132 return fmt::format("\"{}\"", block.begin);
133 }
134}
135} // Anonymous namespace
136
137void Stack::Push(Token token, Location target) {
138 entries.push_back({
139 .token = token,
140 .target{target},
141 });
142}
143
144std::pair<Location, Stack> Stack::Pop(Token token) const {
145 const std::optional<Location> pc{Peek(token)};
146 if (!pc) {
147 throw LogicError("Token could not be found");
148 }
149 return {*pc, Remove(token)};
150}
151
152std::optional<Location> Stack::Peek(Token token) const {
153 const auto it{std::find_if(entries.rbegin(), entries.rend(),
154 [token](const auto& entry) { return entry.token == token; })};
155 if (it == entries.rend()) {
156 return std::nullopt;
157 }
158 return it->target;
159}
160
161Stack Stack::Remove(Token token) const {
162 const auto it{std::find_if(entries.rbegin(), entries.rend(),
163 [token](const auto& entry) { return entry.token == token; })};
164 const auto pos{std::distance(entries.rbegin(), it)};
165 Stack result;
166 result.entries.insert(result.entries.end(), entries.begin(), entries.end() - pos - 1);
167 return result;
168}
169
170bool Block::Contains(Location pc) const noexcept {
171 return pc >= begin && pc < end;
172}
173
174Function::Function(ObjectPool<Block>& block_pool, Location start_address)
175 : entrypoint{start_address} {
176 Label& label{labels.emplace_back()};
177 label.address = start_address;
178 label.block = block_pool.Create(Block{});
179 label.block->begin = start_address;
180 label.block->end = start_address;
181 label.block->end_class = EndClass::Branch;
182 label.block->cond = IR::Condition(true);
183 label.block->branch_true = nullptr;
184 label.block->branch_false = nullptr;
185}
186
187CFG::CFG(Environment& env_, ObjectPool<Block>& block_pool_, Location start_address,
188 bool exits_to_dispatcher_)
189 : env{env_}, block_pool{block_pool_}, program_start{start_address}, exits_to_dispatcher{
190 exits_to_dispatcher_} {
191 if (exits_to_dispatcher) {
192 dispatch_block = block_pool.Create(Block{});
193 dispatch_block->begin = {};
194 dispatch_block->end = {};
195 dispatch_block->end_class = EndClass::Exit;
196 dispatch_block->cond = IR::Condition(true);
197 dispatch_block->stack = {};
198 dispatch_block->branch_true = nullptr;
199 dispatch_block->branch_false = nullptr;
200 }
201 functions.emplace_back(block_pool, start_address);
202 for (FunctionId function_id = 0; function_id < functions.size(); ++function_id) {
203 while (!functions[function_id].labels.empty()) {
204 Function& function{functions[function_id]};
205 Label label{function.labels.back()};
206 function.labels.pop_back();
207 AnalyzeLabel(function_id, label);
208 }
209 }
210 if (exits_to_dispatcher) {
211 const auto last_block{functions[0].blocks.rbegin()};
212 dispatch_block->begin = last_block->end + 1;
213 dispatch_block->end = last_block->end + 1;
214 functions[0].blocks.insert(*dispatch_block);
215 }
216}
217
218void CFG::AnalyzeLabel(FunctionId function_id, Label& label) {
219 if (InspectVisitedBlocks(function_id, label)) {
220 // Label address has been visited
221 return;
222 }
223 // Try to find the next block
224 Function* const function{&functions[function_id]};
225 Location pc{label.address};
226 const auto next_it{function->blocks.upper_bound(pc, Compare{})};
227 const bool is_last{next_it == function->blocks.end()};
228 Block* const next{is_last ? nullptr : &*next_it};
229 // Insert before the next block
230 Block* const block{label.block};
231 // Analyze instructions until it reaches an already visited block or there's a branch
232 bool is_branch{false};
233 while (!next || pc < next->begin) {
234 is_branch = AnalyzeInst(block, function_id, pc) == AnalysisState::Branch;
235 if (is_branch) {
236 break;
237 }
238 ++pc;
239 }
240 if (!is_branch) {
241 // If the block finished without a branch,
242 // it means that the next instruction is already visited, jump to it
243 block->end = pc;
244 block->cond = IR::Condition{true};
245 block->branch_true = next;
246 block->branch_false = nullptr;
247 }
248 // Function's pointer might be invalid, resolve it again
249 // Insert the new block
250 functions[function_id].blocks.insert(*block);
251}
252
253bool CFG::InspectVisitedBlocks(FunctionId function_id, const Label& label) {
254 const Location pc{label.address};
255 Function& function{functions[function_id]};
256 const auto it{
257 std::ranges::find_if(function.blocks, [pc](auto& block) { return block.Contains(pc); })};
258 if (it == function.blocks.end()) {
259 // Address has not been visited
260 return false;
261 }
262 Block* const visited_block{&*it};
263 if (visited_block->begin == pc) {
264 throw LogicError("Dangling block");
265 }
266 Block* const new_block{label.block};
267 Split(visited_block, new_block, pc);
268 function.blocks.insert(it, *new_block);
269 return true;
270}
271
272CFG::AnalysisState CFG::AnalyzeInst(Block* block, FunctionId function_id, Location pc) {
273 const Instruction inst{env.ReadInstruction(pc.Offset())};
274 const Opcode opcode{Decode(inst.raw)};
275 switch (opcode) {
276 case Opcode::BRA:
277 case Opcode::JMP:
278 case Opcode::RET:
279 if (!AnalyzeBranch(block, function_id, pc, inst, opcode)) {
280 return AnalysisState::Continue;
281 }
282 switch (opcode) {
283 case Opcode::BRA:
284 case Opcode::JMP:
285 AnalyzeBRA(block, function_id, pc, inst, IsAbsoluteJump(opcode));
286 break;
287 case Opcode::RET:
288 block->end_class = EndClass::Return;
289 break;
290 default:
291 break;
292 }
293 block->end = pc;
294 return AnalysisState::Branch;
295 case Opcode::BRK:
296 case Opcode::CONT:
297 case Opcode::LONGJMP:
298 case Opcode::SYNC: {
299 if (!AnalyzeBranch(block, function_id, pc, inst, opcode)) {
300 return AnalysisState::Continue;
301 }
302 const auto [stack_pc, new_stack]{block->stack.Pop(OpcodeToken(opcode))};
303 block->branch_true = AddLabel(block, new_stack, stack_pc, function_id);
304 block->end = pc;
305 return AnalysisState::Branch;
306 }
307 case Opcode::KIL: {
308 const Predicate pred{inst.Pred()};
309 const auto ir_pred{static_cast<IR::Pred>(pred.index)};
310 const IR::Condition cond{inst.branch.flow_test, ir_pred, pred.negated};
311 AnalyzeCondInst(block, function_id, pc, EndClass::Kill, cond);
312 return AnalysisState::Branch;
313 }
314 case Opcode::PBK:
315 case Opcode::PCNT:
316 case Opcode::PEXIT:
317 case Opcode::PLONGJMP:
318 case Opcode::SSY:
319 block->stack.Push(OpcodeToken(opcode), BranchOffset(pc, inst));
320 return AnalysisState::Continue;
321 case Opcode::BRX:
322 case Opcode::JMX:
323 return AnalyzeBRX(block, pc, inst, IsAbsoluteJump(opcode), function_id);
324 case Opcode::EXIT:
325 return AnalyzeEXIT(block, function_id, pc, inst);
326 case Opcode::PRET:
327 throw NotImplementedException("PRET flow analysis");
328 case Opcode::CAL:
329 case Opcode::JCAL: {
330 const bool is_absolute{IsAbsoluteJump(opcode)};
331 const Location cal_pc{is_absolute ? inst.branch.Absolute() : BranchOffset(pc, inst)};
332 // Technically CAL pushes into PRET, but that's implicit in the function call for us
333 // Insert the function into the list if it doesn't exist
334 const auto it{std::ranges::find(functions, cal_pc, &Function::entrypoint)};
335 const bool exists{it != functions.end()};
336 const FunctionId call_id{exists ? static_cast<size_t>(std::distance(functions.begin(), it))
337 : functions.size()};
338 if (!exists) {
339 functions.emplace_back(block_pool, cal_pc);
340 }
341 block->end_class = EndClass::Call;
342 block->function_call = call_id;
343 block->return_block = AddLabel(block, block->stack, pc + 1, function_id);
344 block->end = pc;
345 return AnalysisState::Branch;
346 }
347 default:
348 break;
349 }
350 const Predicate pred{inst.Pred()};
351 if (pred == Predicate{true} || pred == Predicate{false}) {
352 return AnalysisState::Continue;
353 }
354 const IR::Condition cond{static_cast<IR::Pred>(pred.index), pred.negated};
355 AnalyzeCondInst(block, function_id, pc, EndClass::Branch, cond);
356 return AnalysisState::Branch;
357}
358
359void CFG::AnalyzeCondInst(Block* block, FunctionId function_id, Location pc,
360 EndClass insn_end_class, IR::Condition cond) {
361 if (block->begin != pc) {
362 // If the block doesn't start in the conditional instruction
363 // mark it as a label to visit it later
364 block->end = pc;
365 block->cond = IR::Condition{true};
366 block->branch_true = AddLabel(block, block->stack, pc, function_id);
367 block->branch_false = nullptr;
368 return;
369 }
370 // Create a virtual block and a conditional block
371 Block* const conditional_block{block_pool.Create()};
372 Block virtual_block{};
373 virtual_block.begin = block->begin.Virtual();
374 virtual_block.end = block->begin.Virtual();
375 virtual_block.end_class = EndClass::Branch;
376 virtual_block.stack = block->stack;
377 virtual_block.cond = cond;
378 virtual_block.branch_true = conditional_block;
379 virtual_block.branch_false = nullptr;
380 // Save the contents of the visited block in the conditional block
381 *conditional_block = std::move(*block);
382 // Impersonate the visited block with a virtual block
383 *block = std::move(virtual_block);
384 // Set the end properties of the conditional instruction
385 conditional_block->end = pc + 1;
386 conditional_block->end_class = insn_end_class;
387 // Add a label to the instruction after the conditional instruction
388 Block* const endif_block{AddLabel(conditional_block, block->stack, pc + 1, function_id)};
389 // Branch to the next instruction from the virtual block
390 block->branch_false = endif_block;
391 // And branch to it from the conditional instruction if it is a branch or a kill instruction
392 // Kill instructions are considered a branch because they demote to a helper invocation and
393 // execution may continue.
394 if (insn_end_class == EndClass::Branch || insn_end_class == EndClass::Kill) {
395 conditional_block->cond = IR::Condition{true};
396 conditional_block->branch_true = endif_block;
397 conditional_block->branch_false = nullptr;
398 }
399 // Finally insert the condition block into the list of blocks
400 functions[function_id].blocks.insert(*conditional_block);
401}
402
403bool CFG::AnalyzeBranch(Block* block, FunctionId function_id, Location pc, Instruction inst,
404 Opcode opcode) {
405 if (inst.branch.is_cbuf) {
406 throw NotImplementedException("Branch with constant buffer offset");
407 }
408 const Predicate pred{inst.Pred()};
409 if (pred == Predicate{false}) {
410 return false;
411 }
412 const bool has_flow_test{HasFlowTest(opcode)};
413 const IR::FlowTest flow_test{has_flow_test ? inst.branch.flow_test.Value() : IR::FlowTest::T};
414 if (pred != Predicate{true} || flow_test != IR::FlowTest::T) {
415 block->cond = IR::Condition(flow_test, static_cast<IR::Pred>(pred.index), pred.negated);
416 block->branch_false = AddLabel(block, block->stack, pc + 1, function_id);
417 } else {
418 block->cond = IR::Condition{true};
419 }
420 return true;
421}
422
423void CFG::AnalyzeBRA(Block* block, FunctionId function_id, Location pc, Instruction inst,
424 bool is_absolute) {
425 const Location bra_pc{is_absolute ? inst.branch.Absolute() : BranchOffset(pc, inst)};
426 block->branch_true = AddLabel(block, block->stack, bra_pc, function_id);
427}
428
429CFG::AnalysisState CFG::AnalyzeBRX(Block* block, Location pc, Instruction inst, bool is_absolute,
430 FunctionId function_id) {
431 const std::optional brx_table{TrackIndirectBranchTable(env, pc, program_start)};
432 if (!brx_table) {
433 TrackIndirectBranchTable(env, pc, program_start);
434 throw NotImplementedException("Failed to track indirect branch");
435 }
436 const IR::FlowTest flow_test{inst.branch.flow_test};
437 const Predicate pred{inst.Pred()};
438 if (flow_test != IR::FlowTest::T || pred != Predicate{true}) {
439 throw NotImplementedException("Conditional indirect branch");
440 }
441 std::vector<u32> targets;
442 targets.reserve(brx_table->num_entries);
443 for (u32 i = 0; i < brx_table->num_entries; ++i) {
444 u32 target{env.ReadCbufValue(brx_table->cbuf_index, brx_table->cbuf_offset + i * 4)};
445 if (!is_absolute) {
446 target += pc.Offset();
447 }
448 target += static_cast<u32>(brx_table->branch_offset);
449 target += 8;
450 targets.push_back(target);
451 }
452 std::ranges::sort(targets);
453 targets.erase(std::unique(targets.begin(), targets.end()), targets.end());
454
455 block->indirect_branches.reserve(targets.size());
456 for (const u32 target : targets) {
457 Block* const branch{AddLabel(block, block->stack, target, function_id)};
458 block->indirect_branches.push_back({
459 .block = branch,
460 .address = target,
461 });
462 }
463 block->cond = IR::Condition{true};
464 block->end = pc + 1;
465 block->end_class = EndClass::IndirectBranch;
466 block->branch_reg = brx_table->branch_reg;
467 block->branch_offset = brx_table->branch_offset + 8;
468 if (!is_absolute) {
469 block->branch_offset += pc.Offset();
470 }
471 return AnalysisState::Branch;
472}
473
474CFG::AnalysisState CFG::AnalyzeEXIT(Block* block, FunctionId function_id, Location pc,
475 Instruction inst) {
476 const IR::FlowTest flow_test{inst.branch.flow_test};
477 const Predicate pred{inst.Pred()};
478 if (pred == Predicate{false} || flow_test == IR::FlowTest::F) {
479 // EXIT will never be taken
480 return AnalysisState::Continue;
481 }
482 if (exits_to_dispatcher && function_id != 0) {
483 throw NotImplementedException("Dispatch EXIT on external function");
484 }
485 if (pred != Predicate{true} || flow_test != IR::FlowTest::T) {
486 if (block->stack.Peek(Token::PEXIT).has_value()) {
487 throw NotImplementedException("Conditional EXIT with PEXIT token");
488 }
489 const IR::Condition cond{flow_test, static_cast<IR::Pred>(pred.index), pred.negated};
490 if (exits_to_dispatcher) {
491 block->end = pc;
492 block->end_class = EndClass::Branch;
493 block->cond = cond;
494 block->branch_true = dispatch_block;
495 block->branch_false = AddLabel(block, block->stack, pc + 1, function_id);
496 return AnalysisState::Branch;
497 }
498 AnalyzeCondInst(block, function_id, pc, EndClass::Exit, cond);
499 return AnalysisState::Branch;
500 }
501 if (const std::optional<Location> exit_pc{block->stack.Peek(Token::PEXIT)}) {
502 const Stack popped_stack{block->stack.Remove(Token::PEXIT)};
503 block->cond = IR::Condition{true};
504 block->branch_true = AddLabel(block, popped_stack, *exit_pc, function_id);
505 block->branch_false = nullptr;
506 return AnalysisState::Branch;
507 }
508 if (exits_to_dispatcher) {
509 block->cond = IR::Condition{true};
510 block->end = pc;
511 block->end_class = EndClass::Branch;
512 block->branch_true = dispatch_block;
513 block->branch_false = nullptr;
514 return AnalysisState::Branch;
515 }
516 block->end = pc + 1;
517 block->end_class = EndClass::Exit;
518 return AnalysisState::Branch;
519}
520
521Block* CFG::AddLabel(Block* block, Stack stack, Location pc, FunctionId function_id) {
522 Function& function{functions[function_id]};
523 if (block->begin == pc) {
524 // Jumps to itself
525 return block;
526 }
527 if (const auto it{function.blocks.find(pc, Compare{})}; it != function.blocks.end()) {
528 // Block already exists and it has been visited
529 if (function.blocks.begin() != it) {
530 // Check if the previous node is the virtual variant of the label
531 // This won't exist if a virtual node is not needed or it hasn't been visited
532 // If it hasn't been visited and a virtual node is needed, this will still behave as
533 // expected because the node impersonated with its virtual node.
534 const auto prev{std::prev(it)};
535 if (it->begin.Virtual() == prev->begin) {
536 return &*prev;
537 }
538 }
539 return &*it;
540 }
541 // Make sure we don't insert the same layer twice
542 const auto label_it{std::ranges::find(function.labels, pc, &Label::address)};
543 if (label_it != function.labels.end()) {
544 return label_it->block;
545 }
546 Block* const new_block{block_pool.Create()};
547 new_block->begin = pc;
548 new_block->end = pc;
549 new_block->end_class = EndClass::Branch;
550 new_block->cond = IR::Condition(true);
551 new_block->stack = stack;
552 new_block->branch_true = nullptr;
553 new_block->branch_false = nullptr;
554 function.labels.push_back(Label{
555 .address{pc},
556 .block = new_block,
557 .stack{std::move(stack)},
558 });
559 return new_block;
560}
561
562std::string CFG::Dot() const {
563 int node_uid{0};
564
565 std::string dot{"digraph shader {\n"};
566 for (const Function& function : functions) {
567 dot += fmt::format("\tsubgraph cluster_{} {{\n", function.entrypoint);
568 dot += fmt::format("\t\tnode [style=filled];\n");
569 for (const Block& block : function.blocks) {
570 const std::string name{NameOf(block)};
571 const auto add_branch = [&](Block* branch, bool add_label) {
572 dot += fmt::format("\t\t{}->{}", name, NameOf(*branch));
573 if (add_label && block.cond != IR::Condition{true} &&
574 block.cond != IR::Condition{false}) {
575 dot += fmt::format(" [label=\"{}\"]", block.cond);
576 }
577 dot += '\n';
578 };
579 dot += fmt::format("\t\t{};\n", name);
580 switch (block.end_class) {
581 case EndClass::Branch:
582 if (block.cond != IR::Condition{false}) {
583 add_branch(block.branch_true, true);
584 }
585 if (block.cond != IR::Condition{true}) {
586 add_branch(block.branch_false, false);
587 }
588 break;
589 case EndClass::IndirectBranch:
590 for (const IndirectBranch& branch : block.indirect_branches) {
591 add_branch(branch.block, false);
592 }
593 break;
594 case EndClass::Call:
595 dot += fmt::format("\t\t{}->N{};\n", name, node_uid);
596 dot += fmt::format("\t\tN{}->{};\n", node_uid, NameOf(*block.return_block));
597 dot += fmt::format("\t\tN{} [label=\"Call {}\"][shape=square][style=stripped];\n",
598 node_uid, block.function_call);
599 dot += '\n';
600 ++node_uid;
601 break;
602 case EndClass::Exit:
603 dot += fmt::format("\t\t{}->N{};\n", name, node_uid);
604 dot += fmt::format("\t\tN{} [label=\"Exit\"][shape=square][style=stripped];\n",
605 node_uid);
606 ++node_uid;
607 break;
608 case EndClass::Return:
609 dot += fmt::format("\t\t{}->N{};\n", name, node_uid);
610 dot += fmt::format("\t\tN{} [label=\"Return\"][shape=square][style=stripped];\n",
611 node_uid);
612 ++node_uid;
613 break;
614 case EndClass::Kill:
615 dot += fmt::format("\t\t{}->N{};\n", name, node_uid);
616 dot += fmt::format("\t\tN{} [label=\"Kill\"][shape=square][style=stripped];\n",
617 node_uid);
618 ++node_uid;
619 break;
620 }
621 }
622 if (function.entrypoint == 8) {
623 dot += fmt::format("\t\tlabel = \"main\";\n");
624 } else {
625 dot += fmt::format("\t\tlabel = \"Function {}\";\n", function.entrypoint);
626 }
627 dot += "\t}\n";
628 }
629 if (!functions.empty()) {
630 auto& function{functions.front()};
631 if (function.blocks.empty()) {
632 dot += "Start;\n";
633 } else {
634 dot += fmt::format("\tStart -> {};\n", NameOf(*function.blocks.begin()));
635 }
636 dot += fmt::format("\tStart [shape=diamond];\n");
637 }
638 dot += "}\n";
639 return dot;
640}
641
642} // namespace Shader::Maxwell::Flow