Browse Source

graph: flow (first draft)

Michele Caini 3 years ago
parent
commit
bc5dfb9371
6 changed files with 539 additions and 0 deletions
  1. 1 0
      CMakeLists.txt
  2. 1 0
      src/entt/entt.hpp
  3. 290 0
      src/entt/graph/flow.hpp
  4. 7 0
      src/entt/graph/fwd.hpp
  5. 1 0
      test/CMakeLists.txt
  6. 239 0
      test/entt/graph/flow.cpp

+ 1 - 0
CMakeLists.txt

@@ -148,6 +148,7 @@ if(ENTT_INCLUDE_HEADERS)
             $<BUILD_INTERFACE:${EnTT_SOURCE_DIR}/src/entt/entity/storage.hpp>
             $<BUILD_INTERFACE:${EnTT_SOURCE_DIR}/src/entt/entity/view.hpp>
             $<BUILD_INTERFACE:${EnTT_SOURCE_DIR}/src/entt/graph/adjacency_matrix.hpp>
+            $<BUILD_INTERFACE:${EnTT_SOURCE_DIR}/src/entt/graph/flow.hpp>
             $<BUILD_INTERFACE:${EnTT_SOURCE_DIR}/src/entt/graph/fwd.hpp>
             $<BUILD_INTERFACE:${EnTT_SOURCE_DIR}/src/entt/locator/locator.hpp>
             $<BUILD_INTERFACE:${EnTT_SOURCE_DIR}/src/entt/meta/adl_pointer.hpp>

+ 1 - 0
src/entt/entt.hpp

@@ -33,6 +33,7 @@
 #include "entity/storage.hpp"
 #include "entity/view.hpp"
 #include "graph/adjacency_matrix.hpp"
+#include "graph/flow.hpp"
 #include "locator/locator.hpp"
 #include "meta/adl_pointer.hpp"
 #include "meta/container.hpp"

+ 290 - 0
src/entt/graph/flow.hpp

@@ -0,0 +1,290 @@
+#ifndef ENTT_GRAPH_FLOW_HPP
+#define ENTT_GRAPH_FLOW_HPP
+
+#include <algorithm>
+#include <cstddef>
+#include <iterator>
+#include <memory>
+#include <type_traits>
+#include <utility>
+#include <vector>
+#include "../config/config.h"
+#include "../container/dense_map.hpp"
+#include "../container/dense_set.hpp"
+#include "../core/compressed_pair.hpp"
+#include "../core/fwd.hpp"
+#include "../core/iterator.hpp"
+#include "../core/utility.hpp"
+#include "adjacency_matrix.hpp"
+#include "fwd.hpp"
+
+namespace entt {
+
+/**
+ * @brief Utility class for creating task graphs.
+ * @tparam Allocator Type of allocator used to manage memory and elements.
+ */
+template<typename Allocator>
+class basic_flow {
+    using alloc_traits = std::allocator_traits<Allocator>;
+    static_assert(std::is_same_v<typename alloc_traits::value_type, id_type>, "Invalid value type");
+    using task_container_type = dense_set<id_type, identity, std::equal_to<id_type>, typename alloc_traits::template rebind_alloc<id_type>>;
+    using ro_rw_container_type = std::vector<std::pair<std::size_t, bool>, typename alloc_traits::template rebind_alloc<std::pair<std::size_t, bool>>>;
+    using deps_container_type = dense_map<id_type, ro_rw_container_type, identity, std::equal_to<id_type>, typename alloc_traits::template rebind_alloc<std::pair<const id_type, ro_rw_container_type>>>;
+
+public:
+    /*! @brief Allocator type. */
+    using allocator_type = Allocator;
+    /*! @brief Unsigned integer type. */
+    using size_type = std::size_t;
+    /*! @brief Iterable task list. */
+    using iterable = iterable_adaptor<typename task_container_type::const_iterator>;
+
+    /*! @brief Default constructor. */
+    basic_flow()
+        : basic_flow{allocator_type{}} {}
+
+    /**
+     * @brief Constructs a flow builder with a given allocator.
+     * @param allocator The allocator to use.
+     */
+    explicit basic_flow(const allocator_type &allocator)
+        : index{0u, allocator},
+          vertices{},
+          deps{} {}
+
+    /*! @brief Default copy constructor. */
+    basic_flow(const basic_flow &) = default;
+
+    /**
+     * @brief Allocator-extended copy constructor.
+     * @param other The instance to copy from.
+     * @param allocator The allocator to use.
+     */
+    basic_flow(const basic_flow &other, const allocator_type &allocator)
+        : index{other.index.first(), allocator},
+          vertices{other.vertices, allocator},
+          deps{other.deps, allocator} {}
+
+    /*! @brief Default move constructor. */
+    basic_flow(basic_flow &&) noexcept = default;
+
+    /**
+     * @brief Allocator-extended move constructor.
+     * @param other The instance to move from.
+     * @param allocator The allocator to use.
+     */
+    basic_flow(basic_flow &&other, const allocator_type &allocator)
+        : index{other.index.first(), allocator},
+          vertices{std::move(other.vertices), allocator},
+          deps{std::move(other.deps), allocator} {}
+
+    /**
+     * @brief Default copy assignment operator.
+     * @return This flow builder.
+     */
+    basic_flow &operator=(const basic_flow &) = default;
+
+    /**
+     * @brief Default move assignment operator.
+     * @return This flow builder.
+     */
+    basic_flow &operator=(basic_flow &&) noexcept = default;
+
+    /**
+     * @brief Returns the associated allocator.
+     * @return The associated allocator.
+     */
+    [[nodiscard]] constexpr allocator_type get_allocator() const noexcept {
+        return allocator_type{index.second()};
+    }
+
+    /*! @brief Clears the flow builder. */
+    void clear() noexcept {
+        index.first() = 0u;
+        vertices.clear();
+        deps.clear();
+    }
+
+    /**
+     * @brief Exchanges the contents with those of a given flow builder.
+     * @param other Flow builder to exchange the content with.
+     */
+    void swap(basic_flow &other) {
+        using std::swap;
+        std::swap(index, other.index);
+        std::swap(vertices, other.vertices);
+        std::swap(deps, other.deps);
+    }
+
+    /**
+     * @brief Returns the number of tasks.
+     * @return The number of tasks.
+     */
+    [[nodiscard]] size_type size() const noexcept {
+        return vertices.size();
+    }
+
+    /**
+     * @brief Sets the current task.
+     * @param value Task identifier.
+     * @return This flow builder.
+     */
+    basic_flow &task(id_type value) {
+        if(const auto it = vertices.find(value); it == vertices.cend()) {
+            index.first() = vertices.size();
+            vertices.emplace(value);
+        } else {
+            index.first() = static_cast<size_type>(it - vertices.cbegin());
+        }
+
+        return *this;
+    }
+
+    /**
+     * @brief Returns an iterable object to use to _visit_ the tasks.
+     * @return An iterable object to use to _visit_ the tasks.
+     */
+    iterable tasks() const noexcept {
+        return {vertices.cbegin(), vertices.cend()};
+    }
+
+    /**
+     * @brief Assigns a read-only resource to the current task.
+     * @param res Resource identifier.
+     * @return This flow builder.
+     */
+    basic_flow &ro(id_type res) {
+        ENTT_ASSERT(index.first() < vertices.size(), "Invalid task");
+        deps[res].emplace_back(index.first(), false);
+        return *this;
+    }
+
+    /**
+     * @brief Assigns a range of read-only resources to the current task.
+     * @tparam It Type of input iterator.
+     * @param first An iterator to the first element of the range of elements.
+     * @param last An iterator past the last element of the range of elements.
+     * @return This flow builder.
+     */
+    template<typename It>
+    std::enable_if_t<std::is_same_v<std::remove_const_t<typename std::iterator_traits<It>::value_type>, id_type>, basic_flow &>
+    ro(It first, It last) {
+        for(; first != last; ++first) {
+            ro(*first);
+        }
+
+        return *this;
+    }
+
+    /**
+     * @brief Assigns a writable resource to the current task.
+     * @param res Resource identifier.
+     * @return This flow builder.
+     */
+    basic_flow &rw(id_type res) {
+        ENTT_ASSERT(index.first() < vertices.size(), "Invalid task");
+        deps[res].emplace_back(index.first(), true);
+        return *this;
+    }
+
+    /**
+     * @brief Assigns a range of writable resources to the current task.
+     * @tparam It Type of input iterator.
+     * @param first An iterator to the first element of the range of elements.
+     * @param last An iterator past the last element of the range of elements.
+     * @return This flow builder.
+     */
+    template<typename It>
+    std::enable_if_t<std::is_same_v<std::remove_const_t<typename std::iterator_traits<It>::value_type>, id_type>, basic_flow &>
+    rw(It first, It last) {
+        for(; first != last; ++first) {
+            rw(*first);
+        }
+
+        return *this;
+    }
+
+    /**
+     * @brief Generates a task graph for the current content.
+     * @return The adjacency matrix of the task graph.
+     */
+    adjacency_matrix graph() const {
+        const auto length = vertices.size();
+        adjacency_matrix matrix{length};
+
+        // creates the adjacency matrix
+        for(const auto &elem: deps) {
+            const auto last = elem.second.cend();
+            auto it = elem.second.cbegin();
+
+            while(it != last) {
+                if(it->second) {
+                    // rw item
+                    if(auto curr = it++; it != last) {
+                        if(it->second) {
+                            matrix.insert(curr->first, it->first);
+                        } else if(const auto next = std::find_if(it, last, [](const auto &elem) { return elem.second; }); next != last) {
+                            for(; it != next; ++it) {
+                                matrix.insert(curr->first, it->first);
+                                matrix.insert(it->first, next->first);
+                            }
+                        } else {
+                            for(; it != next; ++it) {
+                                matrix.insert(curr->first, it->first);
+                            }
+                        }
+                    }
+                } else {
+                    // ro item (first iteration only)
+                    if(const auto next = std::find_if(it, last, [](const auto &elem) { return elem.second; }); next != last) {
+                        for(; it != next; ++it) {
+                            matrix.insert(it->first, next->first);
+                        }
+                    } else {
+                        it = last;
+                    }
+                }
+            }
+        }
+
+        // computes the transitive closure
+        for(std::size_t vk{}; vk < length; ++vk) {
+            for(std::size_t vi{}; vi < length; ++vi) {
+                for(std::size_t vj{}; vj < length; ++vj) {
+                    if(matrix.contains(vi, vk) && matrix.contains(vk, vj)) {
+                        matrix.insert(vi, vj);
+                    }
+                }
+            }
+        }
+
+        // applies the transitive reduction
+        for(std::size_t vert{}; vert < length; ++vert) {
+            matrix.erase(vert, vert);
+        }
+
+        for(std::size_t vj{}; vj < length; ++vj) {
+            for(std::size_t vi{}; vi < length; ++vi) {
+                if(matrix.contains(vi, vj)) {
+                    for(std::size_t vk{}; vk < length; ++vk) {
+                        if(matrix.contains(vj, vk)) {
+                            matrix.erase(vi, vk);
+                        }
+                    }
+                }
+            }
+        }
+
+        return matrix;
+    }
+
+private:
+    compressed_pair<size_type, allocator_type> index;
+    task_container_type vertices;
+    deps_container_type deps;
+};
+
+} // namespace entt
+
+#endif

+ 7 - 0
src/entt/graph/fwd.hpp

@@ -3,15 +3,22 @@
 
 #include <cstddef>
 #include <memory>
+#include "../core/fwd.hpp"
 
 namespace entt {
 
 template<typename = std::allocator<std::size_t>>
 class basic_adjacency_matrix;
 
+template<typename = std::allocator<id_type>>
+class basic_flow;
+
 /*! @brief Alias declaration for the most common use case. */
 using adjacency_matrix = basic_adjacency_matrix<>;
 
+/*! @brief Alias declaration for the most common use case. */
+using flow = basic_flow<>;
+
 } // namespace entt
 
 #endif

+ 1 - 0
test/CMakeLists.txt

@@ -220,6 +220,7 @@ SETUP_BASIC_TEST(view entt/entity/view.cpp)
 # Test graph
 
 SETUP_BASIC_TEST(adjacency_matrix entt/graph/adjacency_matrix.cpp)
+SETUP_BASIC_TEST(flow entt/graph/flow.cpp)
 
 # Test locator
 

+ 239 - 0
test/entt/graph/flow.cpp

@@ -0,0 +1,239 @@
+#include <gtest/gtest.h>
+#include <entt/core/fwd.hpp>
+#include <entt/core/hashed_string.hpp>
+#include <entt/graph/flow.hpp>
+#include "../common/throwing_allocator.hpp"
+
+TEST(Flow, Constructors) {
+    entt::flow flow{};
+
+    ASSERT_EQ(flow.size(), 0u);
+
+    flow = entt::flow{std::allocator<entt::id_type>{}};
+
+    ASSERT_EQ(flow.size(), 0u);
+
+    flow.task(0);
+    flow.task(3);
+    flow.task(99);
+
+    ASSERT_EQ(flow.size(), 3u);
+
+    entt::flow temp{flow, flow.get_allocator()};
+    entt::flow other{std::move(flow), flow.get_allocator()};
+
+    ASSERT_EQ(flow.size(), 0u);
+    ASSERT_EQ(other.size(), 3u);
+
+    ASSERT_EQ(flow.tasks().cbegin(), flow.tasks().cend());
+    ASSERT_EQ(*++other.tasks().cbegin(), 3);
+}
+
+TEST(Flow, Copy) {
+    entt::flow flow{};
+
+    flow.task(0);
+    flow.task(3);
+    flow.task(99);
+
+    entt::flow other{flow};
+
+    ASSERT_EQ(flow.size(), 3u);
+    ASSERT_EQ(other.size(), 3u);
+
+    ASSERT_EQ(*flow.tasks().cbegin(), 0);
+    ASSERT_EQ(*++other.tasks().cbegin(), 3);
+
+    flow.task(1);
+    other.task(2);
+
+    other = flow;
+
+    ASSERT_EQ(other.size(), 4u);
+    ASSERT_EQ(flow.size(), 4u);
+
+    ASSERT_EQ(*--flow.tasks().cend(), 1);
+    ASSERT_EQ(*--other.tasks().cend(), 1);
+}
+
+TEST(Flow, Move) {
+    entt::flow flow{};
+
+    flow.task(0);
+    flow.task(3);
+    flow.task(99);
+
+    entt::flow other{std::move(flow)};
+
+    ASSERT_EQ(flow.size(), 0u);
+    ASSERT_EQ(other.size(), 3u);
+
+    ASSERT_EQ(flow.tasks().cbegin(), flow.tasks().cend());
+    ASSERT_EQ(*++other.tasks().cbegin(), 3);
+
+    flow = {};
+    flow.task(1);
+    other.task(2);
+
+    other = std::move(flow);
+
+    ASSERT_EQ(other.size(), 1u);
+    ASSERT_EQ(flow.size(), 0u);
+
+    ASSERT_EQ(flow.tasks().cbegin(), flow.tasks().cend());
+    ASSERT_EQ(*other.tasks().cbegin(), 1);
+}
+
+TEST(Flow, Swap) {
+    entt::flow flow{};
+    entt::flow other{};
+
+    flow.task(7);
+
+    ASSERT_EQ(other.size(), 0u);
+    ASSERT_EQ(flow.size(), 1u);
+    ASSERT_EQ(*flow.tasks().cbegin(), 7);
+    ASSERT_EQ(other.tasks().cbegin(), other.tasks().cend());
+
+    flow.swap(other);
+
+    ASSERT_EQ(other.size(), 1u);
+    ASSERT_EQ(flow.size(), 0u);
+    ASSERT_EQ(flow.tasks().cbegin(), flow.tasks().cend());
+    ASSERT_EQ(*other.tasks().cbegin(), 7);
+}
+
+TEST(Flow, Clear) {
+    entt::flow flow{};
+
+    flow.task(0);
+    flow.task(99);
+
+    ASSERT_EQ(flow.size(), 2u);
+    ASSERT_EQ(*flow.tasks().cbegin(), 0);
+    ASSERT_EQ(*--flow.tasks().cend(), 99);
+    ASSERT_EQ(++flow.tasks().cbegin(), --flow.tasks().cend());
+
+    flow.clear();
+
+    ASSERT_EQ(flow.size(), 0u);
+    ASSERT_EQ(flow.tasks().cbegin(), flow.tasks().cend());
+}
+
+TEST(Flow, RO) {
+    entt::flow flow{};
+    flow.task(0).ro(10).task(1).ro(10).ro(11);
+    auto graph = flow.graph();
+
+    ASSERT_EQ(flow.size(), 2u);
+    ASSERT_EQ(flow.size(), graph.size());
+    ASSERT_EQ(graph.edges().cbegin(), graph.edges().cend());
+}
+
+TEST(Flow, RangeRO) {
+    entt::flow flow{};
+    const entt::id_type res[2u]{10, 11};
+    flow.task(0).ro(res, res + 1).task(1).ro(res, res + 2);
+    auto graph = flow.graph();
+
+    ASSERT_EQ(flow.size(), 2u);
+    ASSERT_EQ(flow.size(), graph.size());
+    ASSERT_EQ(graph.edges().cbegin(), graph.edges().cend());
+}
+
+TEST(Flow, RW) {
+    entt::flow flow{};
+    flow.task(0).rw(10).task(1).rw(10).rw(11);
+    auto graph = flow.graph();
+
+    ASSERT_EQ(flow.size(), 2u);
+    ASSERT_EQ(flow.size(), graph.size());
+    ASSERT_NE(graph.edges().cbegin(), graph.edges().cend());
+
+    ASSERT_TRUE(graph.contains(0u, 1u));
+    ASSERT_FALSE(graph.contains(1u, 0u));
+}
+
+TEST(Flow, RangeRW) {
+    entt::flow flow{};
+    const entt::id_type res[2u]{10, 11};
+    flow.task(0).rw(res, res + 1).task(1).rw(res, res + 2);
+    auto graph = flow.graph();
+
+    ASSERT_EQ(flow.size(), 2u);
+    ASSERT_EQ(flow.size(), graph.size());
+    ASSERT_NE(graph.edges().cbegin(), graph.edges().cend());
+
+    ASSERT_TRUE(graph.contains(0u, 1u));
+    ASSERT_FALSE(graph.contains(1u, 0u));
+}
+
+TEST(Flow, Graph) {
+    using namespace entt::literals;
+
+    entt::flow flow{};
+
+    flow.task("task_0"_hs)
+        .ro("resource_0"_hs)
+        .rw("resource_1"_hs);
+
+    flow.task("task_1"_hs)
+        .ro("resource_0"_hs)
+        .rw("resource_2"_hs);
+
+    flow.task("task_2"_hs)
+        .ro("resource_1"_hs)
+        .rw("resource_3"_hs);
+
+    flow.task("task_3"_hs)
+        .rw("resource_1"_hs)
+        .ro("resource_2"_hs);
+
+    flow.task("task_4"_hs)
+        .rw("resource_0"_hs);
+
+    auto graph = flow.graph();
+
+    ASSERT_EQ(flow.size(), 5u);
+    ASSERT_EQ(flow.size(), graph.size());
+
+    auto from = flow.tasks().cbegin();
+    const auto to = flow.tasks().cend();
+
+    ASSERT_NE(from, to);
+    ASSERT_EQ(from[0u], "task_0"_hs);
+    ASSERT_EQ(from[1u], "task_1"_hs);
+    ASSERT_EQ(from[2u], "task_2"_hs);
+    ASSERT_EQ(from[3u], "task_3"_hs);
+    ASSERT_EQ(from[4u], "task_4"_hs);
+    ASSERT_EQ(from + 5u, to);
+
+    auto first = graph.edges().cbegin();
+    const auto last = graph.edges().cend();
+
+    ASSERT_NE(first, last);
+    ASSERT_EQ(*first++, std::make_pair(std::size_t{0u}, std::size_t{2u}));
+    ASSERT_EQ(*first++, std::make_pair(std::size_t{0u}, std::size_t{4u}));
+    ASSERT_EQ(*first++, std::make_pair(std::size_t{1u}, std::size_t{3u}));
+    ASSERT_EQ(*first++, std::make_pair(std::size_t{1u}, std::size_t{4u}));
+    ASSERT_EQ(*first++, std::make_pair(std::size_t{2u}, std::size_t{3u}));
+    ASSERT_EQ(first, last);
+}
+
+TEST(Flow, ThrowingAllocator) {
+    using allocator = test::throwing_allocator<entt::id_type>;
+    using task_allocator = test::throwing_allocator<std::pair<std::size_t, entt::id_type>>;
+    using task_exception = typename task_allocator::exception_type;
+
+    entt::basic_flow<allocator> flow{};
+
+    task_allocator::trigger_on_allocate = true;
+
+    ASSERT_EQ(flow.size(), 0u);
+    ASSERT_THROW(flow.task(1), task_exception);
+    ASSERT_EQ(flow.size(), 0u);
+
+    flow.task(1);
+
+    ASSERT_EQ(flow.size(), 1u);
+}