Helios Engine
A modular ECS based data-oriented C++23 game engine framework
Loading...
Searching...
No Matches
scheduler.cpp
Go to the documentation of this file.
1#include <pch.hpp>
2
4
5#include <helios/assert.hpp>
6#include <helios/ecs/details/profile.hpp>
11#include <helios/log/logger.hpp>
12
13#include <algorithm>
14#include <cstddef>
15#include <expected>
16#include <format>
17#include <queue>
18#include <ranges>
19#include <unordered_map>
20#include <vector>
21
22namespace helios::ecs {
23
24namespace {
25
26struct TopoNode {
27 std::vector<size_t> after;
28 std::vector<size_t> before;
29};
30
31[[nodiscard]] auto TopoSortHashes(
32 const std::vector<size_t>& hashes,
33 const std::unordered_map<size_t, TopoNode>& nodes) -> std::vector<size_t> {
34 const size_t size = hashes.size();
35 if (size == 0) [[unlikely]] {
36 return {};
37 }
38
39 std::unordered_map<size_t, size_t> hash_to_idx;
40 hash_to_idx.reserve(size);
41 for (size_t i = 0; i < size; ++i) {
42 hash_to_idx[hashes[i]] = i;
43 }
44
45 std::vector<std::vector<size_t>> adjacency(size);
46 std::vector<size_t> in_degree(size, 0);
47
48 for (size_t i = 0; i < size; ++i) {
49 const auto& entry = nodes.at(hashes[i]);
50
51 for (const size_t after_hash : entry.after) {
52 if (const auto it = hash_to_idx.find(after_hash);
53 it != hash_to_idx.end()) {
54 adjacency[it->second].push_back(i);
55 ++in_degree[i];
56 }
57 }
58
59 for (const size_t before_hash : entry.before) {
60 if (const auto it = hash_to_idx.find(before_hash);
61 it != hash_to_idx.end()) {
62 adjacency[i].push_back(it->second);
63 ++in_degree[it->second];
64 }
65 }
66 }
67
68 std::queue<size_t> queue;
69 for (size_t i = 0; i < size; ++i) {
70 if (in_degree[i] == 0) {
71 queue.push(i);
72 }
73 }
74
75 std::vector<size_t> order;
76 order.reserve(size);
77
78 while (!queue.empty()) {
79 const size_t current = queue.front();
80 queue.pop();
81 order.push_back(hashes[current]);
82
83 for (size_t succ : adjacency[current]) {
84 if (--in_degree[succ] == 0) {
85 queue.push(succ);
86 }
87 }
88 }
89
90 if (order.size() < size) {
92 "Cycle detected in schedule ordering; {} items will execute in "
93 "arbitrary order",
94 size - order.size());
95 for (size_t i = 0; i < size; ++i) {
96 if (std::ranges::find(order, hashes[i]) == order.end()) {
97 order.push_back(hashes[i]);
98 }
99 }
100 }
101
102 return order;
103}
104
105} // namespace
106
107void Scheduler::Run(World& world, Executor& executor) {
108 HELIOS_ASSERT(!is_dirty_,
109 "Scheduler::Run called but scheduler is dirty! "
110 "Call Build() first.");
111
112 HELIOS_ECS_PROFILE_SCOPE_N("helios::ecs::Scheduler::Run");
113 HELIOS_ECS_PROFILE_ZONE_VALUE(schedules_.size());
114
115 for (const size_t schedule_hash : execution_order_cache_) {
116 const auto it = schedules_.find(schedule_hash);
117 if (it == schedules_.end()) {
118 continue;
119 }
120
121 it->second.schedule.RunAndWait(world, executor);
122 }
123}
124
125void Scheduler::Run(World& world) {
126 HELIOS_ASSERT(!is_dirty_,
127 "Scheduler::Run called but scheduler is dirty! "
128 "Call Scheduler::Build first.");
129
130 HELIOS_ECS_PROFILE_SCOPE_N("helios::ecs::Scheduler::Run");
131 HELIOS_ECS_PROFILE_ZONE_VALUE(schedules_.size());
132
133 for (const size_t schedule_hash : execution_order_cache_) {
134 const auto it = schedules_.find(schedule_hash);
135 if (it == schedules_.end()) {
136 continue;
137 }
138
139 it->second.schedule.RunAndWait(world);
140 }
141}
142
144 HELIOS_ASSERT(!is_dirty_,
145 "Scheduler::RunStage called but scheduler is dirty! "
146 "Call Scheduler::Build first.");
147
148 HELIOS_ECS_PROFILE_SCOPE_N("helios::ecs::Scheduler::RunStage");
149 HELIOS_ECS_PROFILE_ZONE_VALUE(schedules_.size());
150
151 const size_t stage_hash = stage.Hash();
152 HELIOS_ASSERT(stages_.contains(stage_hash),
153 "Stage with hash '{}' is not registered!", stage_hash);
154
155 const auto members_it = stage_member_order_.find(stage_hash);
156 if (members_it == stage_member_order_.end()) {
157 return;
158 }
159
160 for (const size_t member_hash : members_it->second) {
161 const auto it = schedules_.find(member_hash);
162 if (it == schedules_.end()) [[unlikely]] {
163 continue;
164 }
165
166 it->second.schedule.RunAndWait(world);
167 }
168}
169
171 Executor& executor) {
172 HELIOS_ASSERT(!is_dirty_,
173 "Scheduler::RunStage called but scheduler is dirty! "
174 "Call Scheduler::Build first.");
175
176 HELIOS_ECS_PROFILE_SCOPE_N("helios::ecs::Scheduler::RunStage");
177 HELIOS_ECS_PROFILE_ZONE_VALUE(schedules_.size());
178
179 const size_t stage_hash = stage.Hash();
180 HELIOS_ASSERT(stages_.contains(stage_hash),
181 "Stage with hash '{}' is not registered!", stage_hash);
182
183 const auto members_it = stage_member_order_.find(stage_hash);
184 if (members_it == stage_member_order_.end()) {
185 return;
186 }
187
188 for (const size_t member_hash : members_it->second) {
189 const auto it = schedules_.find(member_hash);
190 if (it == schedules_.end()) [[unlikely]] {
191 continue;
192 }
193
194 it->second.schedule.RunAndWait(world, executor);
195 }
196}
197
198void Scheduler::BuildImpl(async::Executor* async_executor) {
199 for (auto& [hash, entry] : schedules_) {
200 if (entry.schedule.GetExecutor() == nullptr) {
201 auto kind = entry.schedule.Settings().executor_kind;
202 if (async_executor == nullptr && kind != ExecutorKind::kMainThread) {
204 }
205 entry.schedule.SetExecutor(CreateExecutor(kind, async_executor));
206 }
207
208 const auto result = entry.schedule.Build();
209 HELIOS_ASSERT(result.has_value(), "Failed to build scheduler: {}!",
210 result.error().message);
211 }
212
213 const auto ordering_result = ValidateOrdering();
214 HELIOS_ASSERT(ordering_result.has_value(),
215 "Failed to validate schedule ordering: {}!",
216 ordering_result.error().message);
217
218 ComputeExecutionOrder();
219 MarkClean();
220}
221
222auto Scheduler::ValidateOrdering() -> ScheduleResult<void> {
223 for (const auto& [hash, entry] : schedules_) {
224 if (entry.stage_hash.has_value() &&
225 !stages_.contains(entry.stage_hash.value())) {
226 return std::unexpected(ScheduleError{
228 .message = std::format(
229 "Schedule '{}' is assigned to unregistered stage hash '{}'",
230 entry.name, entry.stage_hash.value()),
231 .involved_systems = {}});
232 }
233
234 const auto validate_schedule_target =
235 [&](size_t other_hash) -> ScheduleResult<void> {
236 const auto other_it = schedules_.find(other_hash);
237 if (other_it == schedules_.end()) [[unlikely]] {
238 return std::unexpected(ScheduleError{
240 .message =
241 std::format("Unknown schedule referenced by '{}'", entry.name),
242 .involved_systems = {}});
243 }
244
245 const ScheduleEntry& other = other_it->second;
246
247 if (entry.stage_hash != other.stage_hash) {
248 return std::unexpected(ScheduleError{
250 .message = std::format(
251 "Schedule '{}' cannot depend on schedule '{}' from a "
252 "different stage",
253 entry.name, other.name),
254 .involved_systems = {}});
255 }
256
257 return {};
258 };
259
260 for (const size_t after_hash : entry.after_schedules) {
261 if (auto result = validate_schedule_target(after_hash); !result) {
262 return result;
263 }
264 }
265 for (const size_t before_hash : entry.before_schedules) {
266 if (auto result = validate_schedule_target(before_hash); !result) {
267 return result;
268 }
269 }
270 }
271
272 for (const auto& [hash, entry] : stages_) {
273 const auto validate_stage_target =
274 [&](size_t other_hash) -> ScheduleResult<void> {
275 if (!stages_.contains(other_hash)) [[unlikely]] {
276 return std::unexpected(ScheduleError{
278 .message =
279 std::format("Unknown stage referenced by '{}'", entry.name),
280 .involved_systems = {}});
281 }
282 return {};
283 };
284
285 for (const size_t after_hash : entry.after_stages) {
286 if (auto result = validate_stage_target(after_hash); !result) {
287 return result;
288 }
289 }
290 for (const size_t before_hash : entry.before_stages) {
291 if (auto result = validate_stage_target(before_hash); !result) {
292 return result;
293 }
294 }
295 }
296
297 return {};
298}
299
300void Scheduler::ComputeExecutionOrder() {
301 stage_order_.clear();
302 stage_member_order_.clear();
303 execution_order_cache_.clear();
304
305 if (schedules_.empty() && stages_.empty()) [[unlikely]] {
306 return;
307 }
308
309 std::unordered_map<size_t, TopoNode> schedule_topo_nodes;
310 schedule_topo_nodes.reserve(schedules_.size());
311 for (const auto& [hash, entry] : schedules_) {
312 schedule_topo_nodes.emplace(hash,
313 TopoNode{.after = entry.after_schedules,
314 .before = entry.before_schedules});
315 }
316
317 std::unordered_map<size_t, TopoNode> stage_topo_nodes;
318 stage_topo_nodes.reserve(stages_.size());
319 for (const auto& [hash, entry] : stages_) {
320 stage_topo_nodes.emplace(hash, TopoNode{.after = entry.after_stages,
321 .before = entry.before_stages});
322 }
323
324 std::vector<size_t> stage_hashes;
325 stage_hashes.reserve(stages_.size());
326 for (const auto& [hash, entry] : stages_) {
327 stage_hashes.push_back(hash);
328 }
329
330 stage_order_ = TopoSortHashes(stage_hashes, stage_topo_nodes);
331
332 for (const size_t stage_hash : stage_order_) {
333 std::vector<size_t> members;
334 members.reserve(schedules_.size());
335
336 for (const auto& [hash, entry] : schedules_) {
337 if (entry.stage_hash.has_value() &&
338 entry.stage_hash.value() == stage_hash) {
339 members.push_back(hash);
340 }
341 }
342
343 stage_member_order_[stage_hash] =
344 TopoSortHashes(members, schedule_topo_nodes);
345 execution_order_cache_.insert(execution_order_cache_.end(),
346 stage_member_order_[stage_hash].begin(),
347 stage_member_order_[stage_hash].end());
348 }
349
350 std::vector<size_t> ungrouped_hashes;
351 ungrouped_hashes.reserve(schedules_.size());
352 for (const auto& [hash, entry] : schedules_) {
353 if (!entry.stage_hash.has_value()) {
354 ungrouped_hashes.push_back(hash);
355 }
356 }
357
358 const std::vector<size_t> ungrouped_order =
359 TopoSortHashes(ungrouped_hashes, schedule_topo_nodes);
360 execution_order_cache_.insert(execution_order_cache_.end(),
361 ungrouped_order.begin(), ungrouped_order.end());
362}
363
364auto Scheduler::CreateExecutor(ExecutorKind kind,
365 async::Executor* async_executor)
366 -> std::unique_ptr<Executor> {
367 switch (kind) {
369 return std::make_unique<MainThreadExecutor>();
371 HELIOS_ASSERT(async_executor != nullptr,
372 "Scheduler: cannot create SingleThreadedExecutor without "
373 "an async::Executor! Call Build(async::Executor&).");
374 return std::make_unique<SingleThreadedExecutor>(*async_executor);
375 }
377 HELIOS_ASSERT(async_executor != nullptr,
378 "Scheduler: cannot create MultiThreadedExecutor without "
379 "an async::Executor! Call Build(async::Executor&).");
380 return std::make_unique<MultiThreadedExecutor>(*async_executor);
381 }
382 }
383
384 HELIOS_ASSERT(false, "Scheduler: unknown ExecutorKind!");
385 return nullptr;
386}
387
388} // namespace helios::ecs
#define HELIOS_ASSERT(condition,...)
Assertion macro that aborts execution in debug builds.
Definition assert.hpp:259
Manages worker threads and executes task graphs using work-stealing scheduling.
Definition executor.hpp:32
Abstract interface for schedule executors.
Definition executor.hpp:27
void RunStage(StageTypeIndex stage, World &world)
void Run(World &world, Executor &executor)
The World class manages entities with their components and systems.
Definition world.hpp:39
constexpr size_t Hash() const noexcept
Retrieves the hash value associated with this TypeIndex.
auto TopoSortHashes(const std::vector< size_t > &hashes, const std::unordered_map< size_t, TopoNode > &nodes) -> std::vector< size_t >
Definition scheduler.cpp:31
ExecutorKind
Kind of executor to use for schedule execution.
Definition executor.hpp:11
@ kMultiThreaded
Systems run in parallel using work-stealing.
Definition executor.hpp:18
@ kSingleThreaded
Systems run sequentially on a single thread.
Definition executor.hpp:16
std::expected< T, ScheduleError > ScheduleResult
Result type for schedule operations that can fail.
Definition schedule.hpp:173
utils::TypeIndex StageTypeIndex
Type index for stages.
Definition stage.hpp:12
void Warn(std::string_view message) noexcept
Logs a warning message with the default logger.
Definition logger.hpp:530
Error type returned when schedule compilation fails.
Definition schedule.hpp:165