diff --git a/docs/Doxyfile b/docs/Doxyfile
index 34dbcec2..5761f58e 100644
--- a/docs/Doxyfile
+++ b/docs/Doxyfile
@@ -762,7 +762,7 @@ WARNINGS = YES
# will automatically be disabled.
# The default value is: YES.
-WARN_IF_UNDOCUMENTED = YES
+WARN_IF_UNDOCUMENTED = NO
# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for
# potential errors in the documentation, such as not documenting some parameters
@@ -950,7 +950,7 @@ EXAMPLE_RECURSIVE = NO
# that contain images that are to be included in the documentation (see the
# \image command).
-IMAGE_PATH =
+IMAGE_PATH = doxygen-images/graph_grid_layout
# The INPUT_FILTER tag can be used to specify a program that doxygen should
# invoke to filter for each input file. Doxygen will invoke the filter program
@@ -1127,7 +1127,7 @@ IGNORE_PREFIX =
# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output
# The default value is: YES.
-GENERATE_HTML = NO
+GENERATE_HTML = YES
# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a
# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
diff --git a/docs/doxygen-images/graph_grid_layout/graph_grid.svg b/docs/doxygen-images/graph_grid_layout/graph_grid.svg
new file mode 100644
index 00000000..274f9327
--- /dev/null
+++ b/docs/doxygen-images/graph_grid_layout/graph_grid.svg
@@ -0,0 +1,664 @@
+
+
diff --git a/docs/doxygen-images/graph_grid_layout/graph_parent_placement.svg b/docs/doxygen-images/graph_grid_layout/graph_parent_placement.svg
new file mode 100644
index 00000000..9b87b81f
--- /dev/null
+++ b/docs/doxygen-images/graph_grid_layout/graph_parent_placement.svg
@@ -0,0 +1,2104 @@
+
+
diff --git a/src/Cutter.pro b/src/Cutter.pro
index 950dfea7..8512c803 100644
--- a/src/Cutter.pro
+++ b/src/Cutter.pro
@@ -562,6 +562,7 @@ HEADERS += \
common/BugReporting.h \
common/HighDpiPixmap.h \
widgets/GraphLayout.h \
+ widgets/GraphGridLayout.h \
widgets/HexWidget.h \
common/SelectionHighlight.h \
common/Decompiler.h \
@@ -574,9 +575,11 @@ HEADERS += \
common/IOModesController.h \
common/SettingsUpgrade.h \
dialogs/LayoutManager.h \
- common/CutterLayout.h
+ common/CutterLayout.h \
+ common/BinaryTrees.h \
+ common/LinkedListPool.h
-GRAPHVIZ_HEADERS = widgets/GraphGridLayout.h
+GRAPHVIZ_HEADERS = widgets/GraphvizLayout.h
FORMS += \
dialogs/AboutDialog.ui \
diff --git a/src/common/BinaryTrees.h b/src/common/BinaryTrees.h
new file mode 100644
index 00000000..56fef37b
--- /dev/null
+++ b/src/common/BinaryTrees.h
@@ -0,0 +1,419 @@
+#ifndef BINARY_TREES_H
+#define BINARY_TREES_H
+
+/** \file BinaryTrees.h
+ * \brief Utilities to simplify creation of specialized augmented binary trees.
+ */
+
+#include
+#include
+#include
+#include
+#include
+
+
+/**
+ * Not really a segment tree for storing segments as referred in academic literature. Can be considered a
+ * full, almost perfect, augmented binary tree. In the context of competitive programming often called segment tree.
+ *
+ * Child classes are expected to implement updateFromChildren(NodeType&parent, NodeType& left, NodeType& right)
+ * method which calculates inner node values from children nodes.
+ *
+ * \tparam NodeTypeT type of each tree element
+ * \tparam FinalType final child class used for curiously recurring template pattern
+ */
+template
+class SegmentTreeBase
+{
+public:
+ using NodePosition = size_t;
+ using NodeType = NodeTypeT;
+
+ /**
+ * @brief Create tree with \a size leaves.
+ * @param size number of leaves in the tree
+ */
+ explicit SegmentTreeBase(size_t size)
+ : size(size)
+ , nodeCount(2 * size)
+ , nodes(nodeCount)
+ {}
+
+ /**
+ * @brief Create a tree with given size and initial value.
+ *
+ * Inner nodes are calculated from leaves.
+ * @param size number of leaves
+ * @param initialValue initial leave value
+ */
+ SegmentTreeBase(size_t size, const NodeType &initialValue)
+ : SegmentTreeBase(size)
+ {
+ init(initialValue);
+ }
+protected:
+ // Curiously recurring template pattern
+ FinalType &This()
+ {
+ return static_cast(*this);
+ }
+
+ // Curiously recurring template pattern
+ const FinalType &This() const
+ {
+ return static_cast(*this);
+ }
+
+ size_t leavePositionToIndex(NodePosition pos) const
+ {
+ return pos - size;
+ }
+
+ NodePosition leaveIndexToPosition(size_t index) const
+ {
+ return index + size;
+ }
+
+ bool isLeave(NodePosition position) const
+ {
+ return position >= size;
+ }
+
+ /**
+ * @brief Calculate inner node values from leaves.
+ */
+ void buildInnerNodes()
+ {
+ for (size_t i = size - 1; i > 0; i--) {
+ This().updateFromChildren(nodes[i], nodes[i << 1], nodes[(i << 1) | 1]);
+ }
+ }
+
+ /**
+ * @brief Initialize leaves with given value.
+ * @param value value that will be assigned to leaves
+ */
+ void init(const NodeType &value)
+ {
+ std::fill_n(nodes.begin() + size, size, value);
+ buildInnerNodes();
+ }
+
+ const size_t size; //< number of leaves and also index of left most leave
+ const size_t nodeCount;
+ std::vector nodes;
+};
+
+/**
+ * \brief Tree for point modification and range queries.
+ */
+template
+class PointSetSegmentTree : public SegmentTreeBase
+{
+ using BaseType = SegmentTreeBase;
+public:
+ using BaseType::BaseType;
+
+ /**
+ * @brief Set leave \a index to \a value.
+ * @param index Leave index, should be in the range [0,size)
+ * @param value
+ */
+ void set(size_t index, const NodeType &value)
+ {
+ auto pos = this->leaveIndexToPosition(index);
+ this->nodes[pos] = value;
+ while (pos > 1) {
+ auto parrent = pos >> 1;
+ this->This().updateFromChildren(this->nodes[parrent], this->nodes[pos], this->nodes[pos ^ 1]);
+ pos = parrent;
+ }
+ }
+
+ const NodeType &valueAtPoint(size_t index) const
+ {
+ return this->nodes[this->leaveIndexToPosition(index)];
+ }
+
+ // Implement range query when necessary
+};
+
+class PointSetMinTree : public PointSetSegmentTree
+{
+ using BaseType = PointSetSegmentTree;
+public:
+ using NodeType = int;
+
+ using BaseType::BaseType;
+
+ void updateFromChildren(NodeType &parent, NodeType &leftChild, NodeType &rightChild)
+ {
+ parent = std::min(leftChild, rightChild);
+ }
+
+ /**
+ * @brief Find right most position with value than less than given in range [0; position].
+ * @param position inclusive right side of query range
+ * @param value search for position less than this
+ * @return returns the position with searched property or -1 if there is no such position.
+ */
+ int rightMostLessThan(size_t position, int value)
+ {
+ auto isGood = [&](size_t pos) {
+ return nodes[pos] < value;
+ };
+ // right side exclusive range [l;r)
+ size_t goodSubtree = 0;
+ for (size_t l = leaveIndexToPosition(0), r = leaveIndexToPosition(position + 1); l < r;
+ l >>= 1, r >>= 1) {
+ if (l & 1) {
+ if (isGood(l)) {
+ // mark subtree as good but don't stop yet, there might be something good further to the right
+ goodSubtree = l;
+ }
+ ++l;
+ }
+ if (r & 1) {
+ --r;
+ if (isGood(r)) {
+ goodSubtree = r;
+ break;
+ }
+ }
+ }
+ if (!goodSubtree) {
+ return -1;
+ }
+ // find rightmost good leave
+ while (goodSubtree < size) {
+ goodSubtree = (goodSubtree << 1) + 1;
+ if (!isGood(goodSubtree)) {
+ goodSubtree ^= 1;
+ }
+ }
+ return leavePositionToIndex(goodSubtree);
+ }
+
+ /**
+ * @brief Find left most position with value less than \a value in range [position; size).
+ * @param position inclusive left side of query range
+ * @param value search for position less than this
+ * @return returns the position with searched property or -1 if there is no such position.
+ */
+ int leftMostLessThan(size_t position, int value)
+ {
+ auto isGood = [&](size_t pos) {
+ return nodes[pos] < value;
+ };
+ // right side exclusive range [l;r)
+ size_t goodSubtree = 0;
+ for (size_t l = leaveIndexToPosition(position), r = leaveIndexToPosition(size); l < r;
+ l >>= 1, r >>= 1) {
+ if (l & 1) {
+ if (isGood(l)) {
+ goodSubtree = l;
+ break;
+ }
+ ++l;
+ }
+ if (r & 1) {
+ --r;
+ if (isGood(r)) {
+ goodSubtree = r;
+ // mark subtree as good but don't stop yet, there might be something good further to the left
+ }
+ }
+ }
+ if (!goodSubtree) {
+ return -1;
+ }
+ // find leftmost good leave
+ while (goodSubtree < size) {
+ goodSubtree = (goodSubtree << 1);
+ if (!isGood(goodSubtree)) {
+ goodSubtree ^= 1;
+ }
+ }
+ return leavePositionToIndex(goodSubtree);
+ }
+};
+
+/**
+ * \brief Tree that supports lazily applying an operation to range.
+ *
+ * Each inner node has a promise value describing an operation that needs to be applied to corresponding subtree.
+ *
+ * Child classes are expected to implement to pushDown(size_t nodePosition) method. Which applies the applies the
+ * operation stored in \a promise for nodePosition to the direct children nodes.
+ *
+ * \tparam NodeType type of tree nodes
+ * \tparam PromiseType type describing operation that needs to be applied to subtree
+ * \tparam FinalType child class type for CRTP. See SegmentTreeBase
+ */
+template
+class LazySegmentTreeBase : public SegmentTreeBase
+{
+ using BaseType = SegmentTreeBase;
+public:
+ /**
+ * @param size Number of tree leaves.
+ * @param neutralPromise Promise value that doesn't modify tree nodes.
+ */
+ LazySegmentTreeBase(size_t size, const PromiseType &neutralPromise)
+ : BaseType(size)
+ , neutralPromiseElement(neutralPromise)
+ , promise(size, neutralPromise)
+ {
+ h = 0;
+ size_t v = size;
+ while (v) {
+ v >>= 1;
+ h++;
+ }
+ }
+
+ LazySegmentTreeBase(size_t size, NodeType value, PromiseType neutralPromise)
+ : LazySegmentTreeBase(size, neutralPromise)
+ {
+ this->init(value);
+ }
+
+ /**
+ * @brief Calculate the tree operation over the range [\a l, \a r)
+ * @param l inclusive range left side
+ * @param r exclusive range right side
+ * @param initialValue Initial value for aggregate operation.
+ * @return Tree operation calculated over the range.
+ */
+ NodeType rangeOperation(size_t l, size_t r, NodeType initialValue)
+ {
+ NodeType result = initialValue;
+ l = this->leaveIndexToPosition(l);
+ r = this->leaveIndexToPosition(r);
+ pushDownFromRoot(l);
+ pushDownFromRoot(r - 1);
+ for (; l < r; l >>= 1, r >>= 1) {
+ if (l & 1) {
+ This().updateFromChildren(result, result, this->nodes[l++]);
+ }
+ if (r & 1) {
+ This().updateFromChildren(result, result, this->nodes[--r]);
+ }
+ }
+ return result;
+ }
+
+protected:
+ /**
+ * @brief Ensure that all the parents of node \a p have the operation applied.
+ * @param p Node position
+ */
+ void pushDownFromRoot(typename BaseType::NodePosition p)
+ {
+ for (size_t i = h; i > 0; i--) {
+ This().pushDown(p >> i);
+ }
+ }
+
+ /**
+ * @brief Update all the inner nodes in path from \a p to root.
+ * @param p node position
+ */
+ void updateUntilRoot(typename BaseType::NodePosition p)
+ {
+ while (p > 1) {
+ auto parent = p >> 1;
+ if (promise[parent] == neutralPromiseElement) {
+ This().updateFromChildren(this->nodes[parent], this->nodes[p & ~size_t(1)], this->nodes[p | 1]);
+ }
+ p = parent;
+ }
+ }
+
+ using BaseType::This;
+
+ int h; //< Tree height
+ const PromiseType neutralPromiseElement;
+ std::vector promise;
+};
+
+
+/**
+ * @brief Structure supporting range assignment and range maximum operations.
+ */
+class RangeAssignMaxTree : public LazySegmentTreeBase
+{
+ using BaseType = LazySegmentTreeBase;
+public:
+ using ValueType = int;
+ RangeAssignMaxTree(size_t size, ValueType initialValue)
+ : BaseType(size, initialValue, 0)
+ {
+ }
+
+ void updateFromChildren(NodeType &parent, const NodeType &left, const NodeType &right)
+ {
+ parent = std::max(left, right);
+ }
+
+ void pushDown(size_t parent)
+ {
+ if (promise[parent]) {
+ size_t left = (parent << 1);
+ size_t right = (parent << 1) | 1;
+ nodes[left] = nodes[right] = nodes[parent];
+ if (left < size) {
+ promise[left] = promise[parent];
+ }
+ if (right < size) {
+ promise[right] = promise[parent];
+ }
+ promise[parent] = neutralPromiseElement;
+ }
+ }
+
+ /**
+ * @brief Change all the elements in range [\a left, \a right) to \a value.
+ * @param left inclusive range left side
+ * @param right exclusive right side of range
+ * @param value value to be assigned
+ */
+ void setRange(size_t left, size_t right, NodeType value)
+ {
+ left = leaveIndexToPosition(left);
+ right = leaveIndexToPosition(right);
+ pushDownFromRoot(left);
+ pushDownFromRoot(right - 1);
+ for (size_t l = left, r = right; l < r; l >>= 1, r >>= 1) {
+ if (l & 1) {
+ nodes[l] = value;
+ if (!isLeave(l)) {
+ promise[l] = 1;
+ }
+ l += 1;
+ }
+ if (r & 1) {
+ r -= 1;
+ nodes[r] = value;
+ if (!isLeave(r)) {
+ promise[r] = 1;
+ }
+ }
+ }
+ updateUntilRoot(left);
+ updateUntilRoot(right - 1);
+ }
+
+ /**
+ * @brief Calculate biggest value in the range [l, r)
+ * @param l inclusive left side of range
+ * @param r exclusive right side of range
+ * @return biggest value in given range
+ */
+ int rangeMaximum(size_t l, size_t r)
+ {
+ return rangeOperation(l, r, std::numeric_limits::min());
+ }
+};
+
+#endif // BINARY_TREES_H
diff --git a/src/common/LinkedListPool.h b/src/common/LinkedListPool.h
new file mode 100644
index 00000000..b56340fe
--- /dev/null
+++ b/src/common/LinkedListPool.h
@@ -0,0 +1,170 @@
+#ifndef LINKED_LIST_POOL_H
+#define LINKED_LIST_POOL_H
+
+#include
+#include
+#include
+
+/**
+ * @brief Pool of singly linked lists.
+ *
+ * Should not be used as general purpose container. Use only for algorithms that require linked lists ability
+ * to split and concatenate them. All the data is owned by LinkedListPool.
+ *
+ * In contrast to std::list and std::forward_list doesn't allocate each node separately. LinkedListPool can reserve
+ * all the memory for multiple lists during construction. Uses std::vector as backing container.
+ */
+template
+class LinkedListPool
+{
+ using IndexType = size_t;
+ struct Item {
+ IndexType next;
+ T value;
+ };
+public:
+ /**
+ * @brief Single list within LinkedListPool.
+ *
+ * List only refers to chain of elements. Copying it doesn't copy any element. Item data is owned by
+ * LinkedListPool.
+ *
+ * Use LinkedListPool::makeList to create non-empty list.
+ */
+ class List
+ {
+ IndexType head = 0;
+ IndexType tail = 0;
+ friend class LinkedListPool;
+ List(IndexType head, IndexType tail)
+ : head(head)
+ , tail(tail)
+ {}
+ public:
+ /**
+ * @brief Create an empty list
+ */
+ List() = default;
+ };
+
+ /**
+ * @brief List iterator.
+ *
+ * Iterators don't get invalidated by adding items to list, but the items may be relocated.
+ */
+ class ListIterator
+ {
+ IndexType index = 0;
+ LinkedListPool *pool = nullptr;
+ ListIterator(IndexType index, LinkedListPool *pool)
+ : index(index)
+ , pool(pool)
+ {}
+
+ friend class LinkedListPool;
+ public:
+ using iterator_category = std::forward_iterator_tag;
+ using value_type = T;
+ using difference_type = size_t;
+ using pointer = T*;
+ using reference = T&;
+ ListIterator() = default;
+ reference operator*()
+ {
+ return pool->data[index].value;
+ }
+ ListIterator &operator++()
+ {
+ index = pool->data[index].next;
+ return *this;
+ }
+ ListIterator operator++(int)
+ {
+ ListIterator tmp(*this);
+ operator++();
+ return tmp;
+ }
+ bool operator!=(const ListIterator &b) const
+ {
+ return index != b.index || pool != b.pool;
+ };
+ /**
+ * @brief Test if iterator points to valid value.
+ */
+ operator bool() const
+ {
+ return index;
+ }
+ };
+
+ /**
+ * @brief Create a linked list pool with capacity for \a initialCapacity list items.
+ * @param initialCapacity number of elements to preallocate.
+ */
+ LinkedListPool(size_t initialCapacity)
+ : data(1)
+ {
+ data.reserve(initialCapacity + 1); // [0] element reserved
+ }
+
+ /**
+ * @brief Create a list containing single item.
+ *
+ * Does not invalidate any iterators, but may cause item relocation when initialCapacity is exceeded.
+ * @param value value of element that will be inserted in the created list
+ * @return List containing single value \a value .
+ */
+ List makeList(const T &value)
+ {
+ size_t position = data.size();
+ data.push_back(Item{0, value});
+ return {position, position};
+ }
+
+ /**
+ * @brief Split list and return second half.
+ *
+ * After performing the operation, list passed as argument and return list point to the same items. Modifying them
+ * will affect both lists.
+ *
+ * @param list The list that needs to be split.
+ * @param head Iterator to the first item in new list. Needs to be within \a list .
+ * @return Returns suffix of \a list.
+ */
+ List splitTail(const List &list, const ListIterator &head)
+ {
+ return List {head.index, list.tail};
+ }
+
+ /**
+ * @brief Create list iterator from list.
+ * @param list
+ * @return Iterator pointing to the first item in the list.
+ */
+ ListIterator head(const List &list)
+ {
+ return iteratorFromIndex(list.head);
+ }
+
+ ListIterator end(const List &list)
+ {
+ return std::next(iteratorFromIndex(list.tail));
+ }
+
+ List append(const List &head, const List &tail)
+ {
+ List result{head.head, tail.tail};
+ data[head.tail].next = tail.head;
+ return result;
+ }
+private:
+ ListIterator iteratorFromIndex(IndexType index)
+ {
+ return ListIterator{ index, this };
+ }
+
+ std::vector data;
+};
+
+
+#endif // LINKED_LIST_POOL
diff --git a/src/widgets/DisassemblyWidget.h b/src/widgets/DisassemblyWidget.h
index 9d43a872..0ddaa2d7 100644
--- a/src/widgets/DisassemblyWidget.h
+++ b/src/widgets/DisassemblyWidget.h
@@ -134,7 +134,7 @@ private:
};
/**
- * @class This class is used to draw the left pane of the disassembly
+ * This class is used to draw the left pane of the disassembly
* widget. Its goal is to draw proper arrows for the jumps of the disassembly.
*/
class DisassemblyLeftPanel: public QFrame
diff --git a/src/widgets/GraphGridLayout.cpp b/src/widgets/GraphGridLayout.cpp
index f06cb843..0e57c83f 100644
--- a/src/widgets/GraphGridLayout.cpp
+++ b/src/widgets/GraphGridLayout.cpp
@@ -6,17 +6,142 @@
#include
#include
-// Vector functions
-template
-static void removeFromVec(std::vector &vec, T elem)
-{
- vec.erase(std::remove(vec.begin(), vec.end(), elem), vec.end());
-}
+#include "common/BinaryTrees.h"
+
+
+/** @class GraphGridLayout
+
+Basic familiarity with graph algorithms is recommended.
+
+# Terms used:
+- **Vertex**, **node**, **block** - read description of graph for definition. Within this text vertex and node are
+used interchangeably with block due to code being written for visualizing basic block control flow graph.
+- **edge** - read description of graph for definition for precise definition.
+- **DAG** - directed acyclic graph, graph using directed edges which doesn't have cycles. DAG may contain loops if
+following them would require going in both directions of edges. Example 1->2 1->3 3->2 is a DAG, 2->1 1->3 3->2
+isn't a DAG.
+- **DFS** - depth first search, a graph traversal algorithm
+- **toposort** - topological sorting, process of ordering a DAG vertices that all edges go from vertices earlier in the
+toposort order to vertices later in toposort order. There are multiple algorithms for implementing toposort operation.
+Single DAG can have multiple valid topological orderings, a toposort algorithm can be designed to prioritize a specific
+one from all valid toposort orders. Example: for graph 1->4, 2->1, 2->3, 3->4 valid topological orders are [2,1,3,4] and
+[2,3,1,4].
+
+# High level structure of the algorithm
+1. select subset of edges that form a DAG (remove cycles)
+2. toposort the DAG
+3. choose a subset of edges that form a tree and assign layers
+4. assign node positions within grid using tree structure, child subtrees are placed side by side with parent on top
+5. perform edge routing
+6. calculate column and row pixel positions based on node sizes and amount edges between the rows
+
+
+Contrary to many other layered graph drawing algorithm this implementation doesn't perform node reordering to minimize
+edge crossing. This simplifies implementation, and preserves original control flow structure for conditional jumps (
+true jump on one side, false jump on other). Due to most of control flow being result of structured programming
+constructs like if/then/else and loops, resulting layout is usually readable without node reordering within layers.
+
+
+# Description of grid.
+To simplify the layout algorithm initial steps assume that all nodes have the same size and edges are zero width.
+After placing the nodes and routing the edges it is known which nodes are in in which row and column, how
+many edges are between each pair of rows. Using this information positions are converted from the grid cells
+to pixel coordinates. Routing 0 width edges between rows can also be interpreted as every second row and column being
+reserved for edges. The row numbers in code are using first interpretation. To allow better centering of nodes one
+above other each node is 2 columns wide and 1 row high.
+
+\image html graph_grid.svg
+
+# 1-2 Cycle removal and toposort
+
+Cycle removal and toposort are done at the same time during single DFS traversal. In case entrypoint is part of a loop
+DFS started from entrypoint. This ensures that entrypoint is at the top of resulting layout if possible. Resulting
+toposort order is used in many of the following layout steps that require calculating some property of a vertex based
+on child property or the other way around. Using toposort order such operations can be implemented iteration through
+array in either forward or reverse direction. To prevent running out of stack memory when processing large graphs
+DFS is implemented non-recursively.
+
+# Row assignment
+
+Rows are assigned in toposort order from top to bottom, with nodes row being max(predecessor.row)+1. This ensures
+that loop edges are only ones going from deeper levels to previous layers.
+
+To further simply node placement a subset of edges is selected which forms a tree. This turns DAG drawing problem
+into a tree drawing problem. For each node in level n following nodes which have level exactly n+1 are greedily
+assigned as child nodes in tree. If a node already has parent assigned then corresponding edge is not part of tree.
+
+# Node position assignment
+
+Since the graph has been reduced to a tree, node placement is more or less putting subtrees side by side with
+parent on top. There is some room for interpretation what exactly side by side means and where exactly on top is.
+Drawing the graph either too dense or too big may make it less readable so there are configuration options which allow
+choosing these things resulting in more or less dense layout.
+
+Once the subtrees are placed side by side. Parent node can be placed either in the middle of horizontal bounds or
+in the middle of direct children. First option results in narrower layout and more vertical columns. Second option
+results in nodes being more spread out which may help seeing where each edge goes.
+
+In more compact mode two subtrees are placed side by side taking into account their shape. In wider mode
+bounding box of shorter subtree is used instead of exact shape. This gives slightly sparse layout without it being too
+wide.
+
+\image html graph_parent_placement.svg
+
+# Edge routing
+Edge routing can be split into: main column selection, rough routing, segment offset calculation.
+
+Transition from source to target row is done using single vertical segment. This is called main column.
+
+Rough routing creates the path of edge using up to 5 segments using grid coordinates.
+Due to nodes being placed in a grid. Horizontal segments of edges can't intersect with any nodes.
+The path for edges is chosen so that it consists of at most 5 segments, typically resulting in sideways U shape or
+square Z shape.
+- short vertical segment from node to horizontal line
+- move to empty column
+- vertical segment between starting row and end row, an empty column can always be found, in the worst case there are empty columns at the sides of drawing
+- horizontal segment to target node column
+- short vertical segment connecting to target node
+
+There are 3 special cases:
+- source and target nodes are in the same column with no nodes between - single vertical segment
+- column bellow stating node is empty - segments 1-3 are merged
+- column above target node is empty - segments 3-5 are merged
+Vertical segment intersection with nodes is prevented using a 2d array marking which vertical segments are blocked and
+naively iterating through all rows between start and end at the desired column.
+
+After rough routing segment offsets are calculated relative to their corresponding edge column. This ensures that
+two segments don't overlap. Segment offsets within each column are assigned greedily with some heuristics for
+assignment order to reduce amount of edge crossings and result in more visually pleasing output for a typical CFG
+graph.
+Each segment gets assigned an offset that is maximum of previously assigned offsets overlapping with current
+segment + segment spacing.
+Assignment order is chosen based on:
+* direction of previous and last segment - helps reducing crossings and place the segments between nodes
+* segment length - reduces crossing when segment endpoints have the same structure as valid parentheses expression
+* edge length - establishes some kind of order when single node is connected to many edges, typically a block
+ with switch statement or block after switch statement.
+
+*/
+
GraphGridLayout::GraphGridLayout(GraphGridLayout::LayoutType layoutType)
: GraphLayout({})
-, layoutType(layoutType)
+ , layoutType(layoutType)
{
+ switch (layoutType) {
+ case LayoutType::Narrow:
+ tightSubtreePlacement = true;
+ parentBetweenDirectChild = false;
+ break;
+ case LayoutType::Medium:
+ tightSubtreePlacement = false;
+ parentBetweenDirectChild = false;
+ break;
+ case LayoutType::Wide:
+ tightSubtreePlacement = false;
+ parentBetweenDirectChild = true;
+ break;
+ }
}
std::vector GraphGridLayout::topoSort(LayoutState &state, ut64 entry)
@@ -27,14 +152,16 @@ std::vector GraphGridLayout::topoSort(LayoutState &state, ut64 entry)
// * select backwards/loop edges
// * perform toposort
std::vector blockOrder;
- // 0 - not visited
- // 1 - in stack
- // 2 - visited
- std::unordered_map visited;
+ enum class State : uint8_t {
+ NotVisited = 0,
+ InStack,
+ Visited
+ };
+ std::unordered_map visited;
visited.reserve(state.blocks->size());
std::stack> stack;
auto dfsFragment = [&visited, &blocks, &state, &stack, &blockOrder](ut64 first) {
- visited[first] = 1;
+ visited[first] = State::InStack;
stack.push({first, 0});
while (!stack.empty()) {
auto v = stack.top().first;
@@ -44,16 +171,16 @@ std::vector GraphGridLayout::topoSort(LayoutState &state, ut64 entry)
++stack.top().second;
auto target = block.edges[edge_index].target;
auto &targetState = visited[target];
- if (targetState == 0) {
- targetState = 1;
+ if (targetState == State::NotVisited) {
+ targetState = State::InStack;
stack.push({target, 0});
state.grid_blocks[v].dag_edge.push_back(target);
- } else if (targetState == 2) {
+ } else if (targetState == State::Visited) {
state.grid_blocks[v].dag_edge.push_back(target);
} // else { targetState == 1 in stack, loop edge }
} else {
stack.pop();
- visited[v] = 2;
+ visited[v] = State::Visited;
blockOrder.push_back(v);
}
}
@@ -64,31 +191,38 @@ std::vector GraphGridLayout::topoSort(LayoutState &state, ut64 entry)
// topological order.
dfsFragment(entry);
for (auto &blockIt : blocks) {
- if (!visited[blockIt.first]) {
+ if (visited[blockIt.first] == State::NotVisited) {
dfsFragment(blockIt.first);
}
}
- // assign levels and select tree edges
+ return blockOrder;
+}
+
+void GraphGridLayout::assignRows(GraphGridLayout::LayoutState &state, const std::vector &blockOrder)
+{
for (auto it = blockOrder.rbegin(), end = blockOrder.rend(); it != end; it++) {
auto &block = state.grid_blocks[*it];
- int nextLevel = block.level + 1;
+ int nextLevel = block.row + 1;
for (auto target : block.dag_edge) {
auto &targetBlock = state.grid_blocks[target];
- targetBlock.level = std::max(targetBlock.level, nextLevel);
+ targetBlock.row = std::max(targetBlock.row, nextLevel);
}
}
+}
+
+void GraphGridLayout::selectTree(GraphGridLayout::LayoutState &state)
+{
for (auto &blockIt : state.grid_blocks) {
auto &block = blockIt.second;
for (auto targetId : block.dag_edge) {
auto &targetBlock = state.grid_blocks[targetId];
- if (!targetBlock.has_parent && targetBlock.level == block.level + 1) {
+ if (!targetBlock.has_parent && targetBlock.row == block.row + 1) {
block.tree_edge.push_back(targetId);
targetBlock.has_parent = true;
}
}
}
- return blockOrder;
}
void GraphGridLayout::CalculateLayout(std::unordered_map &blocks, ut64 entry,
@@ -103,180 +237,89 @@ void GraphGridLayout::CalculateLayout(std::unordered_map &bloc
layoutState.grid_blocks[it.first] = block;
}
- auto block_order = topoSort(layoutState, entry);
- computeAllBlockPlacement(block_order, layoutState);
+ auto blockOrder = topoSort(layoutState, entry);
+ computeAllBlockPlacement(blockOrder, layoutState);
for (auto &blockIt : blocks) {
layoutState.edge[blockIt.first].resize(blockIt.second.edges.size());
- }
-
- // Prepare edge routing
- int col_count = 1;
- int row_count = 0;
- for (const auto &blockIt : layoutState.grid_blocks) {
- if (!blockIt.second.has_parent) {
- row_count = std::max(row_count, blockIt.second.row_count);
- col_count += blockIt.second.col_count;
+ for (size_t i = 0; i < blockIt.second.edges.size(); i++) {
+ layoutState.edge[blockIt.first][i].dest = blockIt.second.edges[i].target;
}
}
- row_count += 2;
- EdgesVector horiz_edges, vert_edges;
- horiz_edges.resize(row_count + 1);
- vert_edges.resize(row_count + 1);
- Matrix edge_valid;
- edge_valid.resize(row_count + 1);
- for (int row = 0; row < row_count + 1; row++) {
- horiz_edges[row].resize(col_count + 1);
- vert_edges[row].resize(col_count + 1);
- edge_valid[row].assign(col_count + 1, true);
+ for (const auto &edgeList : layoutState.edge) {
+ auto &startBlock = layoutState.grid_blocks[edgeList.first];
+ startBlock.outputCount++;
+ for (auto &edge : edgeList.second) {
+ auto &targetBlock = layoutState.grid_blocks[edge.dest];
+ targetBlock.inputCount++;
+ }
}
- for (auto &blockIt : layoutState.grid_blocks) {
+ layoutState.columns = 1;
+ layoutState.rows = 1;
+ for (auto &node : layoutState.grid_blocks) {
+ // count is at least index + 1
+ layoutState.rows = std::max(layoutState.rows, size_t(node.second.row) + 1);
+ // block is 2 column wide
+ layoutState.columns = std::max(layoutState.columns, size_t(node.second.col) + 2);
+ }
+
+ layoutState.rowHeight.assign(layoutState.rows, 0);
+ layoutState.columnWidth.assign(layoutState.columns, 0);
+ for (auto &node : layoutState.grid_blocks) {
+ const auto &inputBlock = blocks[node.first];
+ layoutState.rowHeight[node.second.row] = std::max(inputBlock.height,
+ layoutState.rowHeight[node.second.row]);
+ layoutState.columnWidth[node.second.col] = std::max(inputBlock.width / 2,
+ layoutState.columnWidth[node.second.col]);
+ layoutState.columnWidth[node.second.col + 1] = std::max(inputBlock.width / 2,
+ layoutState.columnWidth[node.second.col + 1]);
+ }
+
+ routeEdges(layoutState);
+
+ convertToPixelCoordinates(layoutState, width, height);
+}
+
+void GraphGridLayout::findMergePoints(GraphGridLayout::LayoutState &state) const
+{
+ for (auto &blockIt : state.grid_blocks) {
auto &block = blockIt.second;
- edge_valid[block.row][block.col + 1] = false;
- }
-
- // Perform edge routing
- for (ut64 blockId : block_order) {
- GraphBlock &block = blocks[blockId];
- GridBlock &start = layoutState.grid_blocks[blockId];
- size_t i = 0;
- for (const auto &edge : block.edges) {
- GridBlock &end = layoutState.grid_blocks[edge.target];
- layoutState.edge[blockId][i++] = routeEdge(horiz_edges, vert_edges, edge_valid, start, end);
- }
- }
-
- // Compute edge counts for each row and column
- std::vector col_edge_count, row_edge_count;
- col_edge_count.assign(col_count + 1, 0);
- row_edge_count.assign(row_count + 1, 0);
- for (int row = 0; row < row_count + 1; row++) {
- for (int col = 0; col < col_count + 1; col++) {
- if (int(horiz_edges[row][col].size()) > row_edge_count[row])
- row_edge_count[row] = int(horiz_edges[row][col].size());
- if (int(vert_edges[row][col].size()) > col_edge_count[col])
- col_edge_count[col] = int(vert_edges[row][col].size());
- }
- }
-
-
- //Compute row and column sizes
- std::vector col_width, row_height;
- col_width.assign(col_count + 1, 0);
- row_height.assign(row_count + 1, 0);
- for (auto &blockIt : blocks) {
- GraphBlock &block = blockIt.second;
- GridBlock &grid_block = layoutState.grid_blocks[blockIt.first];
- if ((int(block.width / 2)) > col_width[grid_block.col])
- col_width[grid_block.col] = int(block.width / 2);
- if ((int(block.width / 2)) > col_width[grid_block.col + 1])
- col_width[grid_block.col + 1] = int(block.width / 2);
- if (int(block.height) > row_height[grid_block.row])
- row_height[grid_block.row] = int(block.height);
- }
-
- // Compute row and column positions
- std::vector col_x, row_y;
- col_x.assign(col_count, 0);
- row_y.assign(row_count, 0);
- std::vector col_edge_x(col_count + 1);
- std::vector row_edge_y(row_count + 1);
- int x = layoutConfig.block_horizontal_margin;
- for (int i = 0; i <= col_count; i++) {
- col_edge_x[i] = x;
- x += layoutConfig.block_horizontal_margin * col_edge_count[i];
- if (i < col_count) {
- col_x[i] = x;
- x += col_width[i];
- }
- }
- int y = layoutConfig.block_vertical_margin;
- for (int i = 0; i <= row_count; i++) {
- row_edge_y[i] = y;
- if (!row_edge_count[i]) {
- // prevent 2 blocks being put on top of each other without any space
- row_edge_count[i] = 1;
- }
- y += layoutConfig.block_vertical_margin * row_edge_count[i];
- if (i < row_count) {
- row_y[i] = y;
- y += row_height[i];
- }
- }
- width = x + (layoutConfig.block_horizontal_margin);
- height = y + (layoutConfig.block_vertical_margin);
-
- //Compute node positions
- for (auto &blockIt : blocks) {
- GraphBlock &block = blockIt.second;
- GridBlock &grid_block = layoutState.grid_blocks[blockIt.first];
- auto column = grid_block.col;
- auto row = grid_block.row;
- block.x = int(col_x[column] + col_width[column] +
- ((layoutConfig.block_horizontal_margin / 2) * col_edge_count[column + 1])
- - (block.width / 2));
- if ((block.x + block.width) > (
- col_x[column] + col_width[column] + col_width[column + 1] +
- layoutConfig.block_horizontal_margin *
- col_edge_count[column + 1])) {
- block.x = int((col_x[column] + col_width[column] + col_width[column + 1] +
- layoutConfig.block_horizontal_margin * col_edge_count[column + 1]) - block.width);
- }
- block.y = row_y[row];
- }
-
- // Compute coordinates for edges
- auto position_from_middle = [](int index, int spacing, int column_count) {
- return spacing * (((index & 1) ? 1 : -1) * ((index + 1) / 2) + (column_count - 1) / 2);
- };
- for (auto &blockIt : blocks) {
- GraphBlock &block = blockIt.second;
-
- size_t index = 0;
- assert(block.edges.size() == layoutState.edge[block.entry].size());
- for (GridEdge &edge : layoutState.edge[block.entry]) {
- if (edge.points.empty()) {
- qDebug() << "Warning, unrouted edge.";
- continue;
+ GridBlock *mergeBlock = nullptr;
+ int grandChildCount = 0;
+ for (auto edge : block.tree_edge) {
+ auto &targetBlock = state.grid_blocks[edge];
+ if (targetBlock.tree_edge.size()) {
+ mergeBlock = &state.grid_blocks[targetBlock.tree_edge[0]];
}
- auto start = edge.points[0];
- auto start_col = start.col;
- auto last_index = edge.start_index;
- // This is the start point of the edge.
- auto first_pt = QPoint(col_edge_x[start_col] +
- position_from_middle(last_index, layoutConfig.block_horizontal_margin, col_edge_count[start_col]) +
- (layoutConfig.block_horizontal_margin / 2),
- block.y + block.height);
- auto last_pt = first_pt;
- QPolygonF pts;
- pts.append(last_pt);
-
- for (int i = 0; i < int(edge.points.size()); i++) {
- auto end = edge.points[i];
- auto end_row = end.row;
- auto end_col = end.col;
- auto last_index = end.index;
- QPoint new_pt;
- // block_vertical_margin/2 gives the margin from block to the horizontal lines
- if (start_col == end_col)
- new_pt = QPoint(last_pt.x(), row_edge_y[end_row] +
- position_from_middle(last_index, layoutConfig.block_vertical_margin, row_edge_count[end_row]) +
- (layoutConfig.block_vertical_margin / 2));
- else
- new_pt = QPoint(col_edge_x[end_col] +
- position_from_middle(last_index, layoutConfig.block_horizontal_margin, col_edge_count[end_col]) +
- (layoutConfig.block_horizontal_margin / 2), last_pt.y());
- pts.push_back(new_pt);
- last_pt = new_pt;
- start_col = end_col;
+ grandChildCount += targetBlock.tree_edge.size();
+ }
+ if (!mergeBlock || grandChildCount != 1) {
+ continue;
+ }
+ int blocksGoingToMerge = 0;
+ int blockWithTreeEdge = 0;
+ for (auto edge : block.tree_edge) {
+ auto &targetBlock = state.grid_blocks[edge];
+ bool goesToMerge = false;
+ for (auto secondEdgeTarget : targetBlock.dag_edge) {
+ if (secondEdgeTarget == mergeBlock->id) {
+ goesToMerge = true;
+ break;
+ }
}
-
- const auto &target = blocks[edge.dest];
- auto new_pt = QPoint(last_pt.x(), target.y - 1);
- pts.push_back(new_pt);
- block.edges[index].polyline = pts;
- index++;
+ if (goesToMerge) {
+ if (targetBlock.tree_edge.size() == 1) {
+ blockWithTreeEdge = blocksGoingToMerge;
+ }
+ blocksGoingToMerge++;
+ } else {
+ break;
+ }
+ }
+ if (blocksGoingToMerge) {
+ state.grid_blocks[block.tree_edge[blockWithTreeEdge]].col = blockWithTreeEdge * 2 -
+ (blocksGoingToMerge - 1);
}
}
}
@@ -284,257 +327,763 @@ void GraphGridLayout::CalculateLayout(std::unordered_map &bloc
void GraphGridLayout::computeAllBlockPlacement(const std::vector &blockOrder,
LayoutState &layoutState) const
{
- for (auto blockId : blockOrder) {
- computeBlockPlacement(blockId, layoutState);
- }
- int col = 0;
- for (auto blockId : blockOrder) {
- if (!layoutState.grid_blocks[blockId].has_parent) {
- adjustGraphLayout(layoutState.grid_blocks[blockId], layoutState.grid_blocks, col, 1);
- col += layoutState.grid_blocks[blockId].col_count;
- }
- }
-}
+ assignRows(layoutState, blockOrder);
+ selectTree(layoutState);
+ findMergePoints(layoutState);
-// Prepare graph
-// This computes the position and (row/col based) size of the block
-void GraphGridLayout::computeBlockPlacement(ut64 blockId, LayoutState &layoutState) const
-{
- auto &block = layoutState.grid_blocks[blockId];
- auto &blocks = layoutState.grid_blocks;
- int col = 0;
- int row_count = 1;
- int childColumn = 0;
- bool singleChild = block.tree_edge.size() == 1;
- // Compute all children nodes
- for (size_t i = 0; i < block.tree_edge.size(); i++) {
- ut64 edge = block.tree_edge[i];
- auto &edgeb = blocks[edge];
- row_count = std::max(edgeb.row_count + 1, row_count);
- childColumn = edgeb.col;
- }
- if (layoutType != LayoutType::Wide && block.tree_edge.size() == 2) {
- auto &left = blocks[block.tree_edge[0]];
- auto &right = blocks[block.tree_edge[1]];
- if (left.tree_edge.size() == 0) {
- left.col = right.col - 2;
- int add = left.col < 0 ? - left.col : 0;
- adjustGraphLayout(right, blocks, add, 1);
- adjustGraphLayout(left, blocks, add, 1);
- col = right.col_count + add;
- } else if (right.tree_edge.size() == 0) {
- adjustGraphLayout(left, blocks, 0, 1);
- adjustGraphLayout(right, blocks, left.col + 2, 1);
- col = std::max(left.col_count, right.col + 2);
- } else {
- adjustGraphLayout(left, blocks, 0, 1);
- adjustGraphLayout(right, blocks, left.col_count, 1);
- col = left.col_count + right.col_count;
- }
- block.col_count = std::max(2, col);
- if (layoutType == LayoutType::Medium) {
- block.col = (left.col + right.col) / 2;
- } else {
- block.col = singleChild ? childColumn : (col - 2) / 2;
- }
- } else {
- for (ut64 edge : block.tree_edge) {
- adjustGraphLayout(blocks[edge], blocks, col, 1);
- col += blocks[edge].col_count;
- }
- if (col >= 2) {
- // Place this node centered over the child nodes
- block.col = singleChild ? childColumn : (col - 2) / 2;
- block.col_count = col;
- } else {
- //No child nodes, set single node's width (nodes are 2 columns wide to allow
- //centering over a branch)
+ // Shapes of subtrees are maintained using linked lists. Each value within list is column relative to previous row.
+ // This allows moving things around by changing only first value in list.
+ LinkedListPool sides(blockOrder.size() * 2); // *2 = two sides for each node
+
+ // Process nodes in the order from bottom to top. Ensures that all subtrees are processed before parent node.
+ for (auto blockId : blockOrder) {
+ auto &block = layoutState.grid_blocks[blockId];
+ if (block.tree_edge.size() == 0) {
+ block.row_count = 1;
block.col = 0;
- block.col_count = 2;
- }
- }
- block.row = 0;
- block.row_count = row_count;
-}
+ block.lastRowRight = 2;
+ block.lastRowLeft = 0;
+ block.leftPosition = 0;
+ block.rightPosition = 2;
-void GraphGridLayout::adjustGraphLayout(GridBlock &block,
- std::unordered_map &blocks, int col, int row) const
-{
- block.col += col;
- block.row += row;
- for (ut64 edge : block.tree_edge) {
- adjustGraphLayout(blocks[edge], blocks, col, row);
- }
-}
+ block.leftSideShape = sides.makeList(0);
+ block.rightSideShape = sides.makeList(2);
+ } else {
+ auto &firstChild = layoutState.grid_blocks[block.tree_edge[0]];
+ auto leftSide = firstChild.leftSideShape; // left side of block children subtrees processed so far
+ auto rightSide = firstChild.rightSideShape;
+ block.row_count = firstChild.row_count;
+ block.lastRowRight = firstChild.lastRowRight;
+ block.lastRowLeft = firstChild.lastRowLeft;
+ block.leftPosition = firstChild.leftPosition;
+ block.rightPosition = firstChild.rightPosition;
+ // Place children subtrees side by side
+ for (size_t i = 1; i < block.tree_edge.size(); i++) {
+ auto &child = layoutState.grid_blocks[block.tree_edge[i]];
+ int minPos = INT_MIN;
+ int leftPos = 0;
+ int rightPos = 0;
+ auto leftIt = sides.head(rightSide);
+ auto rightIt = sides.head(child.leftSideShape);
+ int maxLeftWidth = 0;
+ int minRightPos = child.col;
-// Edge computing stuff
-bool GraphGridLayout::isEdgeMarked(EdgesVector &edges, int row, int col, int index)
-{
- if (index >= int(edges[row][col].size()))
- return false;
- return edges[row][col][index];
-}
-
-void GraphGridLayout::markEdge(EdgesVector &edges, int row, int col, int index, bool used)
-{
- while (int(edges[row][col].size()) <= index)
- edges[row][col].push_back(false);
- edges[row][col][index] = used;
-}
-
-GraphGridLayout::GridEdge GraphGridLayout::routeEdge(EdgesVector &horiz_edges,
- EdgesVector &vert_edges,
- Matrix &edge_valid, GridBlock &start, GridBlock &end) const
-{
- GridEdge edge;
- edge.dest = end.id;
-
- //Find edge index for initial outgoing line
- int i = 0;
- while (isEdgeMarked(vert_edges, start.row + 1, start.col + 1, i)) {
- i += 1;
- }
- markEdge(vert_edges, start.row + 1, start.col + 1, i);
- edge.addPoint(start.row + 1, start.col + 1);
- edge.start_index = i;
- bool horiz = false;
-
- //Find valid column for moving vertically to the target node
- int min_row, max_row;
- if (end.row < (start.row + 1)) {
- min_row = end.row;
- max_row = start.row + 1;
- } else {
- min_row = start.row + 1;
- max_row = end.row;
- }
- int col = start.col + 1;
- if (min_row != max_row) {
- auto checkColumn = [min_row, max_row, &edge_valid](int column) {
- if (column < 0 || column >= int(edge_valid[min_row].size()))
- return false;
- for (int row = min_row; row < max_row; row++) {
- if (!edge_valid[row][column]) {
- return false;
+ while (leftIt && rightIt) { // process part of subtrees that touch when put side by side
+ leftPos += *leftIt;
+ rightPos += *rightIt;
+ minPos = std::max(minPos, leftPos - rightPos);
+ maxLeftWidth = std::max(maxLeftWidth, leftPos);
+ minRightPos = std::min(minRightPos, rightPos);
+ ++leftIt;
+ ++rightIt;
}
- }
- return true;
- };
+ int rightTreeOffset = 0;
+ if (tightSubtreePlacement) {
+ rightTreeOffset = minPos; // mode a) place subtrees as close as possible
+ } else {
+ // mode b) use bounding box for shortest subtree and full shape of other side
+ if (leftIt) {
+ rightTreeOffset = maxLeftWidth - child.leftPosition;
+ } else {
+ rightTreeOffset = block.rightPosition - minRightPos;
+ }
- if (!checkColumn(col)) {
- if (checkColumn(end.col + 1)) {
- col = end.col + 1;
+ }
+ // Calculate the new shape after putting the two subtrees side by side
+ child.col += rightTreeOffset;
+ if (leftIt) {
+ *leftIt -= (rightTreeOffset + child.lastRowRight - leftPos);
+ rightSide = sides.append(child.rightSideShape, sides.splitTail(rightSide, leftIt));
+ } else if (rightIt) {
+ *rightIt += (rightPos + rightTreeOffset - block.lastRowLeft);
+ leftSide = sides.append(leftSide, sides.splitTail(child.leftSideShape, rightIt));
+
+ rightSide = child.rightSideShape;
+ block.lastRowRight = child.lastRowRight + rightTreeOffset;
+ block.lastRowLeft = child.lastRowLeft + rightTreeOffset;
+ } else {
+ rightSide = child.rightSideShape;
+ }
+ *sides.head(rightSide) += rightTreeOffset;
+ block.row_count = std::max(block.row_count, child.row_count);
+ block.leftPosition = std::min(block.leftPosition, child.leftPosition + rightTreeOffset);
+ block.rightPosition = std::max(block.rightPosition, rightTreeOffset + child.rightPosition);
+ }
+
+ int col = 0;
+ // Calculate parent position
+ if (parentBetweenDirectChild) {
+ // mode a) keep one child to the left, other to the right
+ for (auto target : block.tree_edge) {
+ col += layoutState.grid_blocks[target].col;
+ }
+ col /= block.tree_edge.size();
} else {
- int ofs = 0;
- while (true) {
- col = start.col + 1 - ofs;
- if (checkColumn(col)) {
- break;
- }
+ // mode b) somewhere between left most direct child and right most, preferably in the middle of
+ // horizontal dimensions. Results layout looks more like single vertical line.
+ col = (block.rightPosition + block.leftPosition) / 2 - 1;
+ col = std::max(col, layoutState.grid_blocks[block.tree_edge.front()].col - 1);
+ col = std::min(col, layoutState.grid_blocks[block.tree_edge.back()].col + 1);
+ }
+ block.col += col; // += instead of = to keep offset calculated in previous steps
+ block.row_count += 1;
+ block.leftPosition = std::min(block.leftPosition, block.col);
+ block.rightPosition = std::max(block.rightPosition, block.col + 2);
- col = start.col + 1 + ofs;
- if (checkColumn(col)) {
- break;
- }
+ *sides.head(leftSide) -= block.col;
+ block.leftSideShape = sides.append(sides.makeList(block.col), leftSide);
- ofs += 1;
+ *sides.head(rightSide) -= block.col + 2;
+ block.rightSideShape = sides.append(sides.makeList(block.col + 2), rightSide);
+
+ // Keep children positions relative to parent so that moving parent moves whole subtree
+ for (auto target : block.tree_edge) {
+ auto &targetBlock = layoutState.grid_blocks[target];
+ targetBlock.col -= block.col;
+ }
+ }
+ }
+
+ // Calculate root positions. Typical function should have one root node that matches with entrypoint.
+ // There can be more of them in case of switch statement analysis failure, unreahable basic blocks or
+ // using the algorithm for non control flow graphs.
+ int nextEmptyColumn = 0;
+ for (auto &blockIt : layoutState.grid_blocks) {
+ auto &block = blockIt.second;
+ if (block.row == 0) { // place all the roots first
+ auto offset = -block.leftPosition;
+ block.col += nextEmptyColumn + offset;
+ nextEmptyColumn = block.rightPosition + offset;
+ }
+ }
+ // Visit all nodes top to bottom, converting relative positions to absolute.
+ for (auto it = blockOrder.rbegin(), end = blockOrder.rend(); it != end; it++) {
+ auto &block = layoutState.grid_blocks[*it];
+ assert(block.col >= 0);
+ for (auto childId : block.tree_edge) {
+ auto &childBlock = layoutState.grid_blocks[childId];
+ childBlock.col += block.col;
+ }
+ }
+}
+
+void GraphGridLayout::routeEdges(GraphGridLayout::LayoutState &state) const
+{
+ calculateEdgeMainColumn(state);
+ roughRouting(state);
+ elaborateEdgePlacement(state);
+}
+
+void GraphGridLayout::calculateEdgeMainColumn(GraphGridLayout::LayoutState &state) const
+{
+ // Find an empty column as close as possible to start or end block's column.
+ // Use sweep line approach processing events sorted by row top to bottom. Use an appropriate tree structure
+ // to contain blocks above sweep line and query for nearest column which isn't blocked by a block.
+
+ struct Event {
+ size_t blockId;
+ size_t edgeId;
+ int row;
+ enum Type {
+ Edge = 0,
+ Block = 1
+ } type;
+ };
+ // create events
+ std::vector events;
+ events.reserve(state.grid_blocks.size() * 2);
+ for (const auto &it : state.grid_blocks) {
+ events.push_back({it.first, 0, it.second.row, Event::Block});
+ const auto &inputBlock = (*state.blocks)[it.first];
+ int startRow = it.second.row + 1;
+
+ auto gridEdges = state.edge[it.first];
+ gridEdges.resize(inputBlock.edges.size());
+ for (size_t i = 0; i < inputBlock.edges.size(); i++) {
+ auto targetId = inputBlock.edges[i].target;
+ gridEdges[i].dest = targetId;
+ const auto &targetGridBlock = state.grid_blocks[targetId];
+ int endRow = targetGridBlock.row;
+ events.push_back({it.first, i, std::max(startRow, endRow), Event::Edge});
+ }
+ }
+ std::sort(events.begin(), events.end(), [](const Event &a, const Event &b) {
+ if (a.row != b.row) {
+ return a.row < b.row;
+ }
+ return static_cast(a.type) < static_cast(b.type);
+ });
+
+ // process events and choose main column for each edge
+ PointSetMinTree blockedColumns(state.columns + 1, -1);
+ for (const auto &event : events) {
+ if (event.type == Event::Block) {
+ auto block = state.grid_blocks[event.blockId];
+ blockedColumns.set(block.col + 1, event.row);
+ } else {
+ auto block = state.grid_blocks[event.blockId];
+ int column = block.col + 1;
+ auto &edge = state.edge[event.blockId][event.edgeId];
+ const auto &targetBlock = state.grid_blocks[edge.dest];
+ auto topRow = std::min(block.row + 1, targetBlock.row);
+ auto targetColumn = targetBlock.col + 1;
+
+ // Prefer using the same column as starting node, it allows reducing amount of segments.
+ if (blockedColumns.valueAtPoint(column) < topRow) {
+ edge.mainColumn = column;
+ } else if (blockedColumns.valueAtPoint(targetColumn) < topRow) { // next try target block column
+ edge.mainColumn = targetColumn;
+ } else {
+ auto nearestLeft = blockedColumns.rightMostLessThan(column, topRow);
+ auto nearestRight = blockedColumns.leftMostLessThan(column, topRow);
+ // There should always be empty column at the sides of drawing
+ assert(nearestLeft != -1 && nearestRight != -1);
+
+ // Choose closest column. Take into account distance to source and target block columns.
+ auto distanceLeft = column - nearestLeft + abs(targetColumn - nearestLeft);
+ auto distanceRight = nearestRight - column + abs(targetColumn - nearestRight);
+
+ // For upward edges try to make a loop instead of 8 shape,
+ // it is slightly longer but produces less crossing.
+ if (targetBlock.row < block.row) {
+ if (targetColumn < column && blockedColumns.valueAtPoint(column + 1) < topRow &&
+ column - targetColumn <= distanceLeft + 2) {
+ edge.mainColumn = column + 1;
+ continue;
+ } else if (targetColumn > column && blockedColumns.valueAtPoint(column - 1) < topRow &&
+ targetColumn - column <= distanceRight + 2) {
+ edge.mainColumn = column - 1;
+ continue;
+ }
+ }
+
+ if (distanceLeft != distanceRight) {
+ edge.mainColumn = distanceLeft < distanceRight ? nearestLeft : nearestRight;
+ } else {
+ // In case of tie choose based on edge index. Should result in true branches being mostly on one
+ // side, false branches on other side.
+ edge.mainColumn = event.edgeId < state.edge[event.blockId].size() / 2 ? nearestLeft : nearestRight;
}
}
}
}
-
- if (col != (start.col + 1)) {
- //Not in same column, need to generate a line for moving to the correct column
- int min_col, max_col;
- if (col < (start.col + 1)) {
- min_col = col;
- max_col = start.col + 1;
- } else {
- min_col = start.col + 1;
- max_col = col;
- }
- int index = findHorizEdgeIndex(horiz_edges, start.row + 1, min_col, max_col);
- edge.addPoint(start.row + 1, col, index);
- horiz = true;
- }
-
- if (end.row != (start.row + 1)) {
- //Not in same row, need to generate a line for moving to the correct row
- if (col == (start.col + 1))
- markEdge(vert_edges, start.row + 1, start.col + 1, i, false);
- int index = findVertEdgeIndex(vert_edges, col, min_row, max_row);
- if (col == (start.col + 1))
- edge.start_index = index;
- edge.addPoint(end.row, col, index);
- horiz = false;
- }
-
- if (col != (end.col + 1)) {
- //Not in ending column, need to generate a line for moving to the correct column
- int min_col, max_col;
- if (col < (end.col + 1)) {
- min_col = col;
- max_col = end.col + 1;
- } else {
- min_col = end.col + 1;
- max_col = col;
- }
- int index = findHorizEdgeIndex(horiz_edges, end.row, min_col, max_col);
- edge.addPoint(end.row, end.col + 1, index);
- horiz = true;
- }
-
- //If last line was horizontal, choose the ending edge index for the incoming edge
- if (horiz) {
- int index = findVertEdgeIndex(vert_edges, end.col + 1, end.row, end.row);
- edge.points[int(edge.points.size()) - 1].index = index;
- }
-
- return edge;
}
-
-int GraphGridLayout::findHorizEdgeIndex(EdgesVector &edges, int row, int min_col, int max_col)
+void GraphGridLayout::roughRouting(GraphGridLayout::LayoutState &state) const
{
- //Find a valid index
- int i = 0;
- while (true) {
- bool valid = true;
- for (int col = min_col; col < max_col + 1; col++)
- if (isEdgeMarked(edges, row, col, i)) {
- valid = false;
- break;
- }
- if (valid)
- break;
- i++;
- }
+ auto getSpacingOverride = [this](int blockWidth, int edgeCount) {
+ int maxSpacing = blockWidth / edgeCount;
+ if (maxSpacing < layoutConfig.edgeHorizontalSpacing) {
+ return std::max(maxSpacing, 1);
+ }
+ return 0;
+ };
- //Mark chosen index as used
- for (int col = min_col; col < max_col + 1; col++)
- markEdge(edges, row, col, i);
- return i;
+ for (auto &blockIt : state.grid_blocks) {
+ auto &blockEdges = state.edge[blockIt.first];
+ for (size_t i = 0; i < blockEdges.size(); i++) {
+ auto &edge = blockEdges[i];
+ const auto &start = blockIt.second;
+ const auto &target = state.grid_blocks[edge.dest];
+
+ edge.addPoint(start.row + 1, start.col + 1);
+ if (edge.mainColumn != start.col + 1) {
+ edge.addPoint(start.row + 1, start.col + 1, edge.mainColumn < start.col + 1 ? -1 : 1);
+ edge.addPoint(start.row + 1, edge.mainColumn, target.row <= start.row ? -2 : 0);
+ }
+ int mainColumnKind = 0;
+ if (edge.mainColumn < start.col + 1 && edge.mainColumn < target.col + 1) {
+ mainColumnKind = +2;
+ } else if (edge.mainColumn > start.col + 1 && edge.mainColumn > target.col + 1) {
+ mainColumnKind = -2;
+ } else if (edge.mainColumn == start.col + 1 && edge.mainColumn != target.col + 1) {
+ mainColumnKind = edge.mainColumn < target.col + 1 ? 1 : -1;
+ } else if (edge.mainColumn == target.col + 1 && edge.mainColumn != start.col + 1) {
+ mainColumnKind = edge.mainColumn < start.col + 1 ? 1 : -1;
+ }
+ edge.addPoint(target.row, edge.mainColumn, mainColumnKind);
+ if (target.col + 1 != edge.mainColumn) {
+ edge.addPoint(target.row, target.col + 1, target.row <= start.row ? 2 : 0);
+ edge.addPoint(target.row, target.col + 1, target.col + 1 < edge.mainColumn ? 1 : -1);
+ }
+
+ // reduce edge spacing when there is large amount of edges connected to single block
+ auto startSpacingOverride = getSpacingOverride((*state.blocks)[start.id].width, start.outputCount);
+ auto targetSpacingOverride = getSpacingOverride((*state.blocks)[target.id].width,
+ target.inputCount);
+ edge.points.front().spacingOverride = startSpacingOverride;
+ edge.points.back().spacingOverride = targetSpacingOverride;
+
+
+ int length = 0;
+ for (size_t i = 1; i < edge.points.size(); i++) {
+ length += abs(edge.points[i].row - edge.points[i - 1].row) +
+ abs(edge.points[i].col - edge.points[i - 1].col);
+ }
+ edge.secondaryPriority = 2 * length + (target.row >= start.row ? 1 : 0);
+ }
+ }
}
-int GraphGridLayout::findVertEdgeIndex(EdgesVector &edges, int col, int min_row, int max_row)
+namespace {
+/**
+ * @brief Single segment of an edge. An edge can be drawn using multiple horizontal and vertical segments.
+ * x y meaning matches vertical segments. For horizontal segments axis are swapped.
+ */
+struct EdgeSegment {
+ int y0;
+ int y1;
+ int x;
+ int edgeIndex;
+ int secondaryPriority;
+ int16_t kind;
+ int16_t spacingOverride; //< segment spacing override, 0 if default spacing should be used
+};
+struct NodeSide {
+ int x;
+ int y0;
+ int y1;
+ int size; //< block size in the x axis direction
+};
+}
+
+/**
+ * @brief Calculate segment offsets relative to their column
+ *
+ * Argument naming uses terms for vertical segments, but the function can be used for horizontal segments as well.
+ *
+ * @param segments Segments that need to be processed.
+ * @param edgeOffsets Output argument for returning segment offsets relative to their columns.
+ * @param edgeColumnWidth InOut argument describing how much column with edges take. Initial value used as minimal
+ * value. May be increased to depending on amount of segments in each column and how tightly they are packed.
+ * @param nodeRightSide Right side of nodes. Used to reduce space reserved for edges by placing them between nodes.
+ * @param nodeLeftSide Same as right side.
+ * @param columnWidth
+ * @param H All the segmement and node coordinates y0 and y1 are expected to be in range [0;H)
+ * @param segmentSpacing The expected spacing between two segments in the same column. Actual spacing may be smaller
+ * for nodes with many edges.
+ */
+void calculateSegmentOffsets(
+ std::vector &segments,
+ std::vector &edgeOffsets,
+ std::vector &edgeColumnWidth,
+ std::vector &nodeRightSide,
+ std::vector &nodeLeftSide,
+ const std::vector &columnWidth,
+ size_t H,
+ int segmentSpacing)
{
- //Find a valid index
- int i = 0;
- while (true) {
- bool valid = true;
- for (int row = min_row; row < max_row + 1; row++)
- if (isEdgeMarked(edges, row, col, i)) {
- valid = false;
- break;
- }
- if (valid)
- break;
- i++;
+ for (auto &segment : segments) {
+ if (segment.y0 > segment.y1) {
+ std::swap(segment.y0, segment.y1);
+ }
}
- //Mark chosen index as used
- for (int row = min_row; row < max_row + 1; row++)
- markEdge(edges, row, col, i);
- return i;
+ std::sort(segments.begin(), segments.end(), [](const EdgeSegment & a, const EdgeSegment & b) {
+ if (a.x != b.x) return a.x < b.x;
+ if (a.kind != b.kind) return a.kind < b.kind;
+ auto aSize = a.y1 - a.y0;
+ auto bSize = b.y1 - b.y0;
+ if (aSize != bSize) {
+ if (a.kind != 1) {
+ return aSize < bSize;
+ } else {
+ return aSize > bSize;
+ }
+ }
+ if (a.kind != 1) {
+ return a.secondaryPriority < b.secondaryPriority;
+ } else {
+ return a.secondaryPriority > b.secondaryPriority;
+ }
+ return false;
+ });
+
+ auto compareNode = [](const NodeSide & a, const NodeSide & b) {
+ return a.x < b.x;
+ };
+ sort(nodeRightSide.begin(), nodeRightSide.end(), compareNode);
+ sort(nodeLeftSide.begin(), nodeLeftSide.end(), compareNode);
+
+ RangeAssignMaxTree maxSegment(H, INT_MIN);
+ auto nextSegmentIt = segments.begin();
+ auto rightSideIt = nodeRightSide.begin();
+ auto leftSideIt = nodeLeftSide.begin();
+ while (nextSegmentIt != segments.end()) {
+ int x = nextSegmentIt->x;
+
+ int leftColumWidth = 0;
+ if (x > 0) {
+ leftColumWidth = columnWidth[x - 1];
+ }
+ maxSegment.setRange(0, H, -leftColumWidth);
+ while (rightSideIt != nodeRightSide.end() && rightSideIt->x + 1 < x) {
+ rightSideIt++;
+ }
+ while (rightSideIt != nodeRightSide.end() && rightSideIt->x + 1 == x) {
+ maxSegment.setRange(rightSideIt->y0, rightSideIt->y1 + 1, rightSideIt->size - leftColumWidth);
+ rightSideIt++;
+ }
+
+ while (nextSegmentIt != segments.end() && nextSegmentIt->x == x && nextSegmentIt->kind <= 1) {
+ int y = maxSegment.rangeMaximum(nextSegmentIt->y0, nextSegmentIt->y1 + 1);
+ if (nextSegmentIt->kind != -2) {
+ y = std::max(y, 0);
+ }
+ y += nextSegmentIt->spacingOverride ? nextSegmentIt->spacingOverride : segmentSpacing;
+ maxSegment.setRange(nextSegmentIt->y0, nextSegmentIt->y1 + 1, y);
+ edgeOffsets[nextSegmentIt->edgeIndex] = y;
+ nextSegmentIt++;
+ }
+
+ auto firstRightSideSegment = nextSegmentIt;
+ auto middleWidth = std::max(maxSegment.rangeMaximum(0, H), 0);
+
+ int rightColumnWidth = 0;
+ if (x < static_cast(columnWidth.size())) {
+ rightColumnWidth = columnWidth[x];
+ }
+
+ maxSegment.setRange(0, H, -rightColumnWidth);
+ while (leftSideIt != nodeLeftSide.end() && leftSideIt->x < x) {
+ leftSideIt++;
+ }
+ while (leftSideIt != nodeLeftSide.end() && leftSideIt->x == x) {
+ maxSegment.setRange(leftSideIt->y0, leftSideIt->y1 + 1, leftSideIt->size - rightColumnWidth);
+ leftSideIt++;
+ }
+ while (nextSegmentIt != segments.end() && nextSegmentIt->x == x) {
+ int y = maxSegment.rangeMaximum(nextSegmentIt->y0, nextSegmentIt->y1 + 1);
+ y += nextSegmentIt->spacingOverride ? nextSegmentIt->spacingOverride : segmentSpacing;
+ maxSegment.setRange(nextSegmentIt->y0, nextSegmentIt->y1 + 1, y);
+ edgeOffsets[nextSegmentIt->edgeIndex] = y;
+ nextSegmentIt++;
+ }
+ auto rightSideMiddle = std::max(maxSegment.rangeMaximum(0, H), 0);
+ rightSideMiddle = std::max(rightSideMiddle, edgeColumnWidth[x] - middleWidth - segmentSpacing);
+ for (auto it = firstRightSideSegment; it != nextSegmentIt; ++it) {
+ edgeOffsets[it->edgeIndex] = middleWidth + (rightSideMiddle - edgeOffsets[it->edgeIndex]) +
+ segmentSpacing;
+ }
+ edgeColumnWidth[x] = middleWidth + segmentSpacing + rightSideMiddle;
+ }
}
+
+
+/**
+ * @brief Center the segments to the middle of edge columns when possible.
+ * @param segmentOffsets offsets relative to the left side edge column.
+ * @param edgeColumnWidth widths of edge columns
+ * @param segments either all horizontal or all vertical edge segments
+ * @param minSpacing spacing between segments
+ */
+static void centerEdges(
+ std::vector &segmentOffsets,
+ const std::vector &edgeColumnWidth,
+ const std::vector &segments,
+ int minSpacing)
+{
+ /* Split segments in each edge column into non intersecting chunks. Center each chunk separately.
+ *
+ * Process segment endpoints sorted by x and y. Maintain count of currently started segments. When number of
+ * active segments reaches 0 there is empty space between chunks.
+ */
+ struct Event {
+ int x;
+ int y;
+ int index;
+ bool start;
+ };
+ std::vector events;
+ events.reserve(segments.size() * 2);
+ for (const auto &segment : segments) {
+ auto offset = segmentOffsets[segment.edgeIndex];
+ // Exclude segments which are outside edge column and between the blocks. It's hard to ensure that moving
+ // them doesn't cause overlap with blocks.
+ if (offset >= 0 && offset <= edgeColumnWidth[segment.x]) {
+ events.push_back({segment.x, segment.y0, segment.edgeIndex, true});
+ events.push_back({segment.x, segment.y1, segment.edgeIndex, false});
+ }
+ }
+ std::sort(events.begin(), events.end(), [](const Event & a, const Event & b) {
+ if (a.x != b.x) return a.x < b.x;
+ if (a.y != b.y) return a.y < b.y;
+ // Process segment start events before end to ensure that activeSegmentCount doesn't go negative and only
+ // reaches 0 at the end of chunk.
+ return int(a.start) > int(b.start);
+ });
+
+ auto it = events.begin();
+ while (it != events.end()) {
+ auto chunkStart = it++;
+ int activeSegmentCount = 1;
+ int chunkWidth = 0;
+ while (activeSegmentCount > 0) {
+ activeSegmentCount += it->start ? 1 : -1;
+ chunkWidth = std::max(chunkWidth, segmentOffsets[it->index]);
+ it++;
+ }
+ // leftMost segment position includes padding on the left side so add it on the right side as well
+ chunkWidth += minSpacing;
+
+ int spacing = (std::max(edgeColumnWidth[chunkStart->x], minSpacing) - chunkWidth) / 2;
+ for (auto segment = chunkStart; segment != it; segment++) {
+ if (segment->start) {
+ segmentOffsets[segment->index] += spacing;
+ }
+ }
+ }
+}
+
+/**
+ * @brief Convert segment coordinates from arbitary range to continuous range starting at 0.
+ * @param segments
+ * @param leftSides
+ * @param rightSides
+ * @return Size of compressed coordinate range.
+ */
+static int compressCoordinates(std::vector &segments,
+ std::vector &leftSides,
+ std::vector &rightSides)
+{
+ std::vector positions;
+ positions.reserve((segments.size() + leftSides.size()) * 2);
+ for (const auto &segment : segments) {
+ positions.push_back(segment.y0);
+ positions.push_back(segment.y1);
+ }
+ for (const auto &segment : leftSides) {
+ positions.push_back(segment.y0);
+ positions.push_back(segment.y1);
+ }
+ // y0 and y1 in rightSides should match leftSides
+
+ std::sort(positions.begin(), positions.end());
+ auto lastUnique = std::unique(positions.begin(), positions.end());
+ positions.erase(lastUnique, positions.end());
+
+ auto positionToIndex = [&] (int position) {
+ size_t index = std::lower_bound(positions.begin(), positions.end(), position) - positions.begin();
+ assert(index < positions.size());
+ return index;
+ };
+ for (auto &segment : segments) {
+ segment.y0 = positionToIndex(segment.y0);
+ segment.y1 = positionToIndex(segment.y1);
+ }
+ assert(leftSides.size() == rightSides.size());
+ for (size_t i = 0; i < leftSides.size(); i++) {
+ leftSides[i].y0 = rightSides[i].y0 = positionToIndex(leftSides[i].y0);
+ leftSides[i].y1 = rightSides[i].y1 = positionToIndex(leftSides[i].y1);
+ }
+ return positions.size();
+}
+
+void GraphGridLayout::elaborateEdgePlacement(GraphGridLayout::LayoutState &state) const
+{
+ int edgeIndex = 0;
+
+ auto segmentFromPoint =
+ [&edgeIndex](const Point & point, const GridEdge & edge, int y0, int y1, int x) {
+ EdgeSegment segment;
+ segment.y0 = y0;
+ segment.y1 = y1;
+ segment.x = x;
+ segment.edgeIndex = edgeIndex++;
+ segment.kind = point.kind;
+ segment.spacingOverride = point.spacingOverride;
+ segment.secondaryPriority = edge.secondaryPriority;
+ return segment;
+ };
+
+
+ std::vector segments;
+ std::vector rightSides;
+ std::vector leftSides;
+ std::vector edgeOffsets;
+
+ // Vertical segments
+ for (auto &edgeListIt : state.edge) {
+ for (const auto &edge : edgeListIt.second) {
+ for (size_t j = 1; j < edge.points.size(); j += 2) {
+ segments.push_back(segmentFromPoint(edge.points[j], edge,
+ edge.points[j-1].row * 2, // edges in even rows
+ edge.points[j].row * 2,
+ edge.points[j].col));
+ }
+ }
+ }
+ for (auto &blockIt : state.grid_blocks) {
+ auto &node = blockIt.second;
+ auto width = (*state.blocks)[blockIt.first].width;
+ auto leftWidth = width / 2;
+ // not the same as leftWidth, you would think that one pixel offset isn't visible, but it is
+ auto rightWidth = width - leftWidth;
+ int row = node.row * 2 + 1; // blocks in odd rows
+ leftSides.push_back({node.col, row, row, leftWidth});
+ rightSides.push_back({node.col + 1, row, row, rightWidth});
+ }
+ state.edgeColumnWidth.assign(state.columns + 1, layoutConfig.blockHorizontalSpacing);
+ state.edgeColumnWidth[0] = state.edgeColumnWidth.back() = layoutConfig.edgeHorizontalSpacing;
+ edgeOffsets.resize(edgeIndex);
+ calculateSegmentOffsets(segments, edgeOffsets, state.edgeColumnWidth, rightSides, leftSides,
+ state.columnWidth, 2 * state.rows + 1, layoutConfig.edgeHorizontalSpacing);
+ centerEdges(edgeOffsets, state.edgeColumnWidth, segments, layoutConfig.blockHorizontalSpacing);
+ edgeIndex = 0;
+
+ auto copySegmentsToEdges = [&](bool col) {
+ int edgeIndex = 0;
+ for (auto &edgeListIt : state.edge) {
+ for (auto &edge : edgeListIt.second) {
+ for (size_t j = col ? 1 : 2; j < edge.points.size(); j += 2) {
+ edge.points[j].offset = edgeOffsets[edgeIndex++];
+ }
+ }
+ }
+ };
+ auto oldColumnWidths = state.columnWidth;
+ adjustColumnWidths(state);
+ for (auto &segment : segments) {
+ auto &offset = edgeOffsets[segment.edgeIndex];
+ if (segment.kind == -2) {
+ offset -= (state.edgeColumnWidth[segment.x - 1] / 2 + state.columnWidth[segment.x - 1]) -
+ oldColumnWidths[segment.x - 1];
+ } else if (segment.kind == 2) {
+ offset += (state.edgeColumnWidth[segment.x + 1] / 2 + state.columnWidth[segment.x]) -
+ oldColumnWidths[segment.x];
+ }
+ }
+ calculateColumnOffsets(state.columnWidth, state.edgeColumnWidth,
+ state.columnOffset, state.edgeColumnOffset);
+ copySegmentsToEdges(true);
+
+
+ // Horizontal segments
+ // Use exact x coordinates obtained from vertical segment placment.
+ segments.clear();
+ leftSides.clear();
+ rightSides.clear();
+
+ edgeIndex = 0;
+ for (auto &edgeListIt : state.edge) {
+ for (const auto &edge : edgeListIt.second) {
+ for (size_t j = 2; j < edge.points.size(); j += 2) {
+ int y0 = state.edgeColumnOffset[edge.points[j - 1].col] + edge.points[j - 1].offset;
+ int y1 = state.edgeColumnOffset[edge.points[j + 1].col] + edge.points[j + 1].offset;
+ segments.push_back(segmentFromPoint(edge.points[j], edge, y0, y1, edge.points[j].row));
+ }
+ }
+ }
+ edgeOffsets.resize(edgeIndex);
+ for (auto &blockIt : state.grid_blocks) {
+ auto &node = blockIt.second;
+ auto blockWidth = (*state.blocks)[node.id].width;
+ int leftSide = state.edgeColumnOffset[node.col + 1] + state.edgeColumnWidth[node.col + 1] / 2 -
+ blockWidth / 2;
+ int rightSide = leftSide + blockWidth;
+
+ int h = (*state.blocks)[blockIt.first].height;
+ int freeSpace = state.rowHeight[node.row] - h;
+ int topProfile = state.rowHeight[node.row];
+ int bottomProfile = h;
+ if (verticalBlockAlignmentMiddle) {
+ topProfile -= freeSpace / 2;
+ bottomProfile += freeSpace / 2;
+ }
+ leftSides.push_back({node.row, leftSide, rightSide, topProfile});
+ rightSides.push_back({node.row, leftSide, rightSide, bottomProfile});
+ }
+ state.edgeRowHeight.assign(state.rows + 1, layoutConfig.blockVerticalSpacing);
+ state.edgeRowHeight[0] = state.edgeRowHeight.back() = layoutConfig.edgeVerticalSpacing;
+ edgeOffsets.resize(edgeIndex);
+ auto compressedCoordinates = compressCoordinates(segments, leftSides, rightSides);
+ calculateSegmentOffsets(segments, edgeOffsets, state.edgeRowHeight, rightSides, leftSides,
+ state.rowHeight, compressedCoordinates, layoutConfig.edgeVerticalSpacing);
+ copySegmentsToEdges(false);
+}
+
+void GraphGridLayout::adjustColumnWidths(GraphGridLayout::LayoutState &state) const
+{
+ state.rowHeight.assign(state.rows, 0);
+ state.columnWidth.assign(state.columns, 0);
+ for (auto &node : state.grid_blocks) {
+ const auto &inputBlock = (*state.blocks)[node.first];
+ state.rowHeight[node.second.row] = std::max(inputBlock.height, state.rowHeight[node.second.row]);
+ int edgeWidth = state.edgeColumnWidth[node.second.col + 1];
+ int columnWidth = (inputBlock.width - edgeWidth) / 2;
+ state.columnWidth[node.second.col] = std::max(columnWidth, state.columnWidth[node.second.col]);
+ state.columnWidth[node.second.col + 1] = std::max(columnWidth,
+ state.columnWidth[node.second.col + 1]);
+ }
+}
+
+int GraphGridLayout::calculateColumnOffsets(const std::vector &columnWidth,
+ std::vector &edgeColumnWidth, std::vector &columnOffset,
+ std::vector &edgeColumnOffset)
+{
+ assert(edgeColumnWidth.size() == columnWidth.size() + 1);
+ int position = 0;
+ edgeColumnOffset.resize(edgeColumnWidth.size());
+ columnOffset.resize(columnWidth.size());
+ for (size_t i = 0; i < columnWidth.size(); i++) {
+ edgeColumnOffset[i] = position;
+ position += edgeColumnWidth[i];
+ columnOffset[i] = position;
+ position += columnWidth[i];
+ }
+ edgeColumnOffset.back() = position;
+ position += edgeColumnWidth.back();
+ return position;
+}
+
+void GraphGridLayout::convertToPixelCoordinates(
+ GraphGridLayout::LayoutState &state,
+ int &width,
+ int &height) const
+{
+ // calculate row and column offsets
+ width = calculateColumnOffsets(state.columnWidth, state.edgeColumnWidth,
+ state.columnOffset, state.edgeColumnOffset);
+ height = calculateColumnOffsets(state.rowHeight, state.edgeRowHeight,
+ state.rowOffset, state.edgeRowOffset);
+
+ // block pixel positions
+ for (auto &block : (*state.blocks)) {
+ const auto &gridBlock = state.grid_blocks[block.first];
+
+ block.second.x = state.edgeColumnOffset[gridBlock.col + 1] +
+ state.edgeColumnWidth[gridBlock.col + 1] / 2 - block.second.width / 2;
+ block.second.y = state.rowOffset[gridBlock.row];
+ if (verticalBlockAlignmentMiddle) {
+ block.second.y += (state.rowHeight[gridBlock.row] - block.second.height) / 2;
+ }
+ }
+ // edge pixel positions
+ for (auto &it : (*state.blocks)) {
+ auto &block = it.second;
+ for (size_t i = 0; i < block.edges.size(); i++) {
+ auto &resultEdge = block.edges[i];
+ const auto &target = (*state.blocks)[resultEdge.target];
+ resultEdge.polyline.clear();
+ resultEdge.polyline.push_back(QPointF(0, block.y + block.height));
+
+ const auto &edge = state.edge[it.first][i];
+ for (size_t j = 1; j < edge.points.size(); j++) {
+ if (j & 1) { // vertical segment
+ int column = edge.points[j].col;
+ int x = state.edgeColumnOffset[column] + edge.points[j].offset;
+ resultEdge.polyline.back().setX(x);
+ resultEdge.polyline.push_back(QPointF(x, 0));
+ } else { // horizontal segment
+ int row = edge.points[j].row;
+ int y = state.edgeRowOffset[row] + edge.points[j].offset;
+ resultEdge.polyline.back().setY(y);
+ resultEdge.polyline.push_back(QPointF(0, y));
+ }
+ }
+ resultEdge.polyline.back().setY(target.y);
+ }
+ }
+}
+
diff --git a/src/widgets/GraphGridLayout.h b/src/widgets/GraphGridLayout.h
index 9e6b1525..34b9a179 100644
--- a/src/widgets/GraphGridLayout.h
+++ b/src/widgets/GraphGridLayout.h
@@ -3,7 +3,12 @@
#include "core/Cutter.h"
#include "GraphLayout.h"
+#include "common/LinkedListPool.h"
+
+/**
+ * @brief Graph layout algorithm on layered graph layout approach. For simplicity all the nodes are placed in a grid.
+ */
class GraphGridLayout : public GraphLayout
{
public:
@@ -20,42 +25,53 @@ public:
int &height) const override;
private:
LayoutType layoutType;
+ /// false - use bounding box for smallest subtree when placing them side by side
+ bool tightSubtreePlacement = false;
+ /// true if code should try to place parent between direct children as much as possible
+ bool parentBetweenDirectChild = false;
+ /// false if blocks in rows should be aligned at top, true for middle alignment
+ bool verticalBlockAlignmentMiddle = false;
struct GridBlock {
ut64 id;
- std::vector tree_edge; // subset of outgoing edges that form a tree
- std::vector dag_edge; // subset of outgoing edges that form a tree
+ std::vector tree_edge; //!< subset of outgoing edges that form a tree
+ std::vector dag_edge; //!< subset of outgoing edges that form a dag
std::size_t has_parent = false;
- int level = 0;
+ int inputCount = 0;
+ int outputCount = 0;
- // Number of rows in block
+ /// Number of rows in subtree
int row_count = 0;
- // Number of columns in block
- int col_count = 0;
- // Column in which the block is
+ /// Column in which the block is
int col = 0;
- // Row in which the block is
+ /// Row in which the block is
int row = 0;
+
+ int lastRowLeft; //!< left side of subtree last row
+ int lastRowRight; //!< right side of subtree last row
+ int leftPosition; //!< left side of subtree
+ int rightPosition; //!< right side of subtree
+ LinkedListPool::List leftSideShape;
+ LinkedListPool::List rightSideShape;
};
struct Point {
- int row; //point[0]
- int col; //point[1]
- int index; //point[2]
+ int row;
+ int col;
+ int offset;
+ int16_t kind;
+ int16_t spacingOverride;
};
struct GridEdge {
ut64 dest;
+ int mainColumn = -1;
std::vector points;
- int start_index = 0;
- QPolygonF polyline;
+ int secondaryPriority;
- void addPoint(int row, int col, int index = 0)
+ void addPoint(int row, int col, int16_t kind = 0)
{
- Point point = {row, col, 0};
- this->points.push_back(point);
- if (int(this->points.size()) > 1)
- this->points[this->points.size() - 2].index = index;
+ this->points.push_back({row, col, 0, kind, 0});
}
};
@@ -63,29 +79,92 @@ private:
std::unordered_map grid_blocks;
std::unordered_map *blocks = nullptr;
std::unordered_map> edge;
+ size_t rows = -1;
+ size_t columns = -1;
+ std::vector columnWidth;
+ std::vector rowHeight;
+ std::vector edgeColumnWidth;
+ std::vector edgeRowHeight;
+
+ std::vector columnOffset;
+ std::vector rowOffset;
+ std::vector edgeColumnOffset;
+ std::vector edgeRowOffset;
};
using GridBlockMap = std::unordered_map;
+ /**
+ * @brief Find nodes where control flow merges after splitting.
+ * Sets node column offset so that after computing placement merge point is centered bellow nodes above.
+ */
+ void findMergePoints(LayoutState &state) const;
+ /**
+ * @brief Compute node rows and columns within grid.
+ * @param blockOrder Nodes in the reverse topological order.
+ */
void computeAllBlockPlacement(const std::vector &blockOrder,
LayoutState &layoutState) const;
- void computeBlockPlacement(ut64 blockId,
- LayoutState &layoutState) const;
- void adjustGraphLayout(GridBlock &block, GridBlockMap &blocks,
- int col, int row) const;
+ /**
+ * @brief Perform the topological sorting of graph nodes.
+ * If the graph contains loops, a subset of edges is selected. Subset of edges forming DAG are stored in
+ * GridBlock::dag_edge.
+ * @param state Graph layout state including the input graph.
+ * @param entry Entrypoint node. When removing loops prefer placing this node at top.
+ * @return Reverse topological ordering.
+ */
static std::vector topoSort(LayoutState &state, ut64 entry);
- // Edge computing stuff
- template
- using Matrix = std::vector>;
- using EdgesVector = Matrix>;
+ /**
+ * @brief Assign row positions to nodes.
+ * @param state
+ * @param blockOrder reverse topological ordering of nodes
+ */
+ static void assignRows(LayoutState &state, const std::vector &blockOrder);
+ /**
+ * @brief Select subset of DAG edges that form tree.
+ * @param state
+ */
+ static void selectTree(LayoutState &state);
- GridEdge routeEdge(EdgesVector &horiz_edges, EdgesVector &vert_edges,
- Matrix &edge_valid, GridBlock &start, GridBlock &end) const;
- static int findVertEdgeIndex(EdgesVector &edges, int col, int min_row, int max_row);
- static bool isEdgeMarked(EdgesVector &edges, int row, int col, int index);
- static void markEdge(EdgesVector &edges, int row, int col, int index, bool used = true);
- static int findHorizEdgeIndex(EdgesVector &edges, int row, int min_col, int max_col);
+ /**
+ * @brief routeEdges Route edges, expects node positions to be calculated previously.
+ */
+ void routeEdges(LayoutState &state) const;
+ /**
+ * @brief Choose which column to use for transition from start node row to target node row.
+ */
+ void calculateEdgeMainColumn(LayoutState &state) const;
+ /**
+ * @brief Do rough edge routing within grid using up to 5 segments.
+ */
+ void roughRouting(LayoutState &state) const;
+ /**
+ * @brief Calculate segment placement relative to their columns.
+ */
+ void elaborateEdgePlacement(LayoutState &state) const;
+ /**
+ * @brief Recalculate column widths, trying to compensate for the space taken by edge columns.
+ */
+ void adjustColumnWidths(LayoutState &state) const;
+ /**
+ * @brief Calculate position of each column(or row) based on widths.
+ * It is assumed that columnWidth.size() + 1 = edgeColumnWidth.size() and they are interleaved.
+ * @param columnWidth
+ * @param edgeColumnWidth
+ * @param columnOffset
+ * @param edgeColumnOffset
+ * @return total width of all the columns
+ */
+ static int calculateColumnOffsets(const std::vector &columnWidth, std::vector &edgeColumnWidth,
+ std::vector &columnOffset, std::vector &edgeColumnOffset);
+ /**
+ * @brief Final graph layout step. Convert grids cell relative positions to absolute pixel positions.
+ * @param state
+ * @param width image width output argument
+ * @param height image height output argument
+ */
+ void convertToPixelCoordinates(LayoutState &state, int &width, int &height) const;
};
#endif // GRAPHGRIDLAYOUT_H
diff --git a/src/widgets/GraphLayout.h b/src/widgets/GraphLayout.h
index 6da66e1b..c5229391 100644
--- a/src/widgets/GraphLayout.h
+++ b/src/widgets/GraphLayout.h
@@ -32,8 +32,10 @@ public:
using Graph = std::unordered_map;
struct LayoutConfig {
- int block_vertical_margin = 40;
- int block_horizontal_margin = 10;
+ int blockVerticalSpacing = 40;
+ int blockHorizontalSpacing = 10;
+ int edgeVerticalSpacing = 10;
+ int edgeHorizontalSpacing = 10;
};
GraphLayout(const LayoutConfig &layout_config) : layoutConfig(layout_config) {}