Helios Engine
A modular ECS based data-oriented C++23 game engine framework
Loading...
Searching...
No Matches
task_graph.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <helios/assert.hpp>
5#include <helios/async/details/profile.hpp>
7
8#include <taskflow/algorithm/for_each.hpp>
9#include <taskflow/algorithm/reduce.hpp>
10#include <taskflow/algorithm/sort.hpp>
11#include <taskflow/algorithm/transform.hpp>
12#include <taskflow/taskflow.hpp>
13
14#include <array>
15#include <concepts>
16#include <cstddef>
17#include <functional>
18#include <ranges>
19#include <string>
20#include <string_view>
21#include <type_traits>
22#include <utility>
23#include <vector>
24
25namespace helios::async {
26
27/**
28 * @brief Represents a task dependency graph that can be executed by an
29 * Executor.
30 * @details Wraps `tf::Taskflow` and provides methods to create tasks, manage
31 * dependencies, and build complex computational graphs. Tasks within the graph
32 * can have dependencies that determine execution order.
33 * @note Not thread-safe.
34 * Do not modify a graph while it's being executed.
35 */
36class TaskGraph {
37public:
38 /**
39 * @brief Constructs a task graph with the given name.
40 * @param name Name for the task graph (default: "TaskGraph")
41 */
42 explicit TaskGraph(std::string_view name = "TaskGraph") { Name(name); }
43 TaskGraph(const TaskGraph&) = delete;
44 TaskGraph(TaskGraph&&) noexcept = default;
45 ~TaskGraph() = default;
46
47 TaskGraph& operator=(const TaskGraph&) = delete;
48 TaskGraph& operator=(TaskGraph&&) noexcept = default;
49
50 /// @brief Clears all tasks and dependencies from this graph.
51 void Clear() { taskflow_.clear(); }
52
53 /**
54 * @brief Applies a visitor function to each task in this graph.
55 * @tparam Visitor Callable type that accepts a Task reference
56 * @param visitor Function to call for each task
57 */
58 template <typename Visitor>
59 requires std::invocable<Visitor, Task&>
60 void ForEachTask(const Visitor& visitor) const;
61
62 /**
63 * @brief Creates a static task with the given callable.
64 * @tparam C Callable type
65 * @param callable Function to execute when the task runs
66 * @return Task handle for the created task
67 */
68 template <StaticTask C>
69 Task EmplaceTask(C&& callable) {
70 return Task(taskflow_.emplace(std::forward<C>(callable)));
71 }
72
73 /**
74 * @brief Creates a dynamic task (subflow) with the given callable.
75 * @details Requires sub_task_graph.hpp to be included.
76 * @tparam C Callable type that accepts a SubTaskGraph reference
77 * @param callable Function to execute when the task runs
78 * @return Task handle for the created task
79 */
80 template <SubTask C>
81 Task EmplaceTask(C&& callable);
82
83 /**
84 * @brief Creates multiple tasks from a list of callables.
85 * @tparam Cs Callable types
86 * @param callables Functions to execute when the tasks run
87 * @return Array of task handles for the created tasks
88 */
89 template <AnyTask... Cs>
90 requires(sizeof...(Cs) > 1)
91 auto EmplaceTasks(Cs&&... callables) -> std::array<Task, sizeof...(Cs)> {
92 return {EmplaceTask(std::forward<Cs>(callables))...};
93 }
94
95 /**
96 * @brief Creates a placeholder task with no assigned work.
97 * @return Task handle that can later be assigned work
98 */
99 Task CreatePlaceholder() { return Task(taskflow_.placeholder()); }
100
101 /**
102 * @brief Creates linear dependencies between tasks in the given range.
103 * @tparam R Range type containing Task objects
104 * @param tasks Range of tasks to linearize (first->second->third->...)
105 */
106 template <std::ranges::range R>
107 requires std::same_as<std::ranges::range_value_t<R>, Task>
108 void Linearize(const R& tasks);
109
110 /**
111 * @brief Creates a parallel for-each task over the given range.
112 * @tparam R Range type
113 * @tparam C Callable type
114 * @param range Input range to iterate over
115 * @param callable Function to apply to each element
116 * @return Task handle for the parallel operation
117 */
118 template <std::ranges::range R, typename C>
119 requires std::invocable<C, std::ranges::range_reference_t<R>>
120 Task ForEach(const R& range, C&& callable) {
121 return Task(taskflow_.for_each(std::ranges::begin(range),
122 std::ranges::end(range),
123 std::forward<C>(callable)));
124 }
125
126 /**
127 * @brief Creates a parallel for-each task over an index range.
128 * @tparam I Integral type
129 * @tparam C Callable type
130 * @param start Starting index (inclusive)
131 * @param end Ending index (exclusive)
132 * @param step Step size
133 * @param callable Function to apply to each index
134 * @return Task handle for the parallel operation
135 */
136 template <std::integral I, typename C>
137 requires std::invocable<C, I>
138 Task ForEachIndex(I start, I end, I step, C&& callable) {
139 return Task(
140 taskflow_.for_each_index(start, end, step, std::forward<C>(callable)));
141 }
142
143 /**
144 * @brief Creates a parallel transform task that applies a function to each
145 * element.
146 * @tparam InputRange Input range type
147 * @tparam OutputRange Output range type
148 * @tparam TransformFunc Transform function type
149 * @param input_range Range of input elements
150 * @param output_range Range to store transformed results
151 * @param transform_func Function to apply to each input element
152 * @return Task handle for the parallel operation
153 */
154 template <
155 std::ranges::range InputRange, std::ranges::range OutputRange,
156 std::invocable<std::ranges::range_reference_t<InputRange>> TransformFunc>
157 Task Transform(const InputRange& input_range, OutputRange& output_range,
158 TransformFunc&& transform_func) {
159 return Task(taskflow_.transform(
160 std::ranges::begin(input_range), std::ranges::end(input_range),
161 std::ranges::begin(output_range),
162 std::forward<TransformFunc>(transform_func)));
163 }
164
165 /**
166 * @brief Creates a parallel reduction task that combines elements using a
167 * binary operation.
168 * @tparam R Range type
169 * @tparam T Result type
170 * @tparam BinaryOp Binary operation type
171 * @param range Range of elements to reduce
172 * @param init Initial value and storage for the result
173 * @param binary_op Binary function to combine elements
174 * @return Task handle for the parallel operation
175 */
176 template <std::ranges::range R, typename T, typename BinaryOp>
177 requires std::invocable<BinaryOp, T, std::ranges::range_reference_t<R>>
178 Task Reduce(const R& range, T& init, BinaryOp&& binary_op) {
179 return Task(taskflow_.reduce(std::ranges::begin(range),
180 std::ranges::end(range), init,
181 std::forward<BinaryOp>(binary_op)));
182 }
183
184 /**
185 * @brief Creates a parallel sort task for the given range.
186 * @tparam R Random access range type
187 * @tparam Compare Comparator type
188 * @param range Range of elements to sort
189 * @param comparator Comparison function (default: std::less<>)
190 * @return Task handle for the parallel operation
191 */
192 template <std::ranges::random_access_range R, typename Compare = std::less<>>
193 requires std::predicate<Compare, std::ranges::range_reference_t<R>,
194 std::ranges::range_reference_t<R>>
195 Task Sort(R& range, Compare&& comparator = Compare{});
196
197 /**
198 * @brief Removes a task from this graph.
199 * @param task Task to remove
200 */
201 void RemoveTask(const Task& task) { taskflow_.erase(task.UnderlyingTask()); }
202
203 /**
204 * @brief Removes a dependency relationship between two tasks.
205 * @param from Source task (dependent)
206 * @param to Target task (successor)
207 */
208 void RemoveDependency(const Task& from, const Task& to) {
209 taskflow_.remove_dependency(from.UnderlyingTask(), to.UnderlyingTask());
210 }
211
212 /**
213 * @brief Creates a module task that encapsulates another task graph.
214 * @param other_graph Task graph to compose into this graph
215 * @return Task handle representing the composed graph
216 */
217 Task Compose(TaskGraph& other_graph) {
218 return Task(taskflow_.composed_of(other_graph.UnderlyingTaskflow()));
219 }
220
221 /**
222 * @brief Dumps the task graph to a DOT format string.
223 * @return String representation of the graph in DOT format
224 */
225 [[nodiscard]] std::string Dump() const { return taskflow_.dump(); }
226
227 /**
228 * @brief Sets the name of this task graph.
229 * @warning Triggers an assertion if the name is empty.
230 * @param name Name for the graph (must not be empty)
231 */
232 void Name(std::string_view name);
233
234 /**
235 * @brief Checks if this graph has no tasks.
236 * @return True if the graph is empty, false otherwise
237 */
238 [[nodiscard]] bool Empty() const { return taskflow_.empty(); }
239
240 /**
241 * @brief Gets the number of tasks in this graph.
242 * @return Count of tasks
243 */
244 [[nodiscard]] size_t TaskCount() const { return taskflow_.num_tasks(); }
245
246 /**
247 * @brief Gets the name of task graph.
248 * @return Name of the graph
249 */
250 [[nodiscard]] const std::string& Name() const { return taskflow_.name(); }
251
252private:
253 [[nodiscard]] tf::Taskflow& UnderlyingTaskflow() noexcept {
254 return taskflow_;
255 }
256
257 [[nodiscard]] const tf::Taskflow& UnderlyingTaskflow() const noexcept {
258 return taskflow_;
259 }
260
261 tf::Taskflow taskflow_;
262
263 friend class SubTaskGraph;
264 friend class Executor;
265};
266
267template <typename Visitor>
268 requires std::invocable<Visitor, Task&>
269inline void TaskGraph::ForEachTask(const Visitor& visitor) const {
270 HELIOS_ASYNC_PROFILE_SCOPE_N("helios::async::TaskGraph::ForEachTask");
271 taskflow_.for_each_task([&visitor](const tf::Task& tf_task) {
272 Task helios_task(tf_task);
273 std::invoke(visitor, helios_task);
274 });
275}
276
277template <std::ranges::range R>
278 requires std::same_as<std::ranges::range_value_t<R>, Task>
279inline void TaskGraph::Linearize(const R& tasks) {
280 HELIOS_ASYNC_PROFILE_SCOPE_N("helios::async::TaskGraph::Linearize");
281
282 std::vector<tf::Task> tf_tasks;
283 if constexpr (std::ranges::sized_range<R>) {
284 tf_tasks.reserve(std::ranges::size(tasks));
285 }
286
287 for (const auto& task : tasks) {
288 tf_tasks.push_back(task.UnderlyingTask());
289 }
290
291 taskflow_.linearize(tf_tasks);
292}
293
294template <std::ranges::random_access_range R, typename Compare>
295 requires std::predicate<Compare, std::ranges::range_reference_t<R>,
296 std::ranges::range_reference_t<R>>
297inline Task TaskGraph::Sort(R& range, Compare&& comparator) {
298 if constexpr (std::same_as<std::remove_cvref_t<Compare>, std::less<>>) {
299 return Task(
300 taskflow_.sort(std::ranges::begin(range), std::ranges::end(range)));
301 } else {
302 return Task(taskflow_.sort(std::ranges::begin(range),
303 std::ranges::end(range),
304 std::forward<Compare>(comparator)));
305 }
306}
307
308inline void TaskGraph::Name(std::string_view name) {
309 HELIOS_ASSERT(!name.empty(), "name cannot be empty!");
310 taskflow_.name(std::string(name));
311}
312
313} // namespace helios::async
#define HELIOS_ASSERT(condition,...)
Assertion macro that aborts execution in debug builds.
Definition assert.hpp:259
Task ForEach(const R &range, C &&callable)
Creates a parallel for-each task over the given range.
const std::string & Name() const
Gets the name of task graph.
Task ForEachIndex(I start, I end, I step, C &&callable)
Creates a parallel for-each task over an index range.
TaskGraph(TaskGraph &&) noexcept=default
bool Empty() const
Checks if this graph has no tasks.
Task CreatePlaceholder()
Creates a placeholder task with no assigned work.
TaskGraph(std::string_view name="TaskGraph")
Constructs a task graph with the given name.
Task Transform(const InputRange &input_range, OutputRange &output_range, TransformFunc &&transform_func)
Creates a parallel transform task that applies a function to each element.
Task Reduce(const R &range, T &init, BinaryOp &&binary_op)
Creates a parallel reduction task that combines elements using a binary operation.
Task Compose(TaskGraph &other_graph)
Creates a module task that encapsulates another task graph.
void Clear()
Clears all tasks and dependencies from this graph.
Task Sort(R &range, Compare &&comparator=Compare{})
Creates a parallel sort task for the given range.
void RemoveTask(const Task &task)
Removes a task from this graph.
void RemoveDependency(const Task &from, const Task &to)
Removes a dependency relationship between two tasks.
std::string Dump() const
Dumps the task graph to a DOT format string.
void ForEachTask(const Visitor &visitor) const
Applies a visitor function to each task in this graph.
void Linearize(const R &tasks)
Creates linear dependencies between tasks in the given range.
Task EmplaceTask(C &&callable)
Creates a static task with the given callable.
auto EmplaceTasks(Cs &&... callables) -> std::array< Task, sizeof...(Cs)>
Creates multiple tasks from a list of callables.
TaskGraph(const TaskGraph &)=delete
size_t TaskCount() const
Gets the number of tasks in this graph.
Represents a single task within a task graph.
Definition task.hpp:25
Concept for any valid task callable.
Definition common.hpp:77