Helios Engine
A modular ECS based data-oriented C++23 game engine framework
Loading...
Searching...
No Matches
schedule.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>
8
9#include <algorithm>
10#include <cstddef>
11#include <expected>
12#include <format>
13#include <iterator>
14#include <ranges>
15#include <span>
16#include <unordered_map>
17#include <utility>
18#include <vector>
19
20namespace helios::ecs {
21
22namespace {
23
25 DagErrorKind kind) noexcept {
26 switch (kind) {
31 default:
33 }
34}
35
36[[nodiscard]] constexpr ScheduleError DagToScheduleError(
37 DagError error) noexcept {
38 return ScheduleError{.kind = DagToScheduleErrorKind(error.kind),
39 .message = std::move(error.message),
40 .involved_systems = std::move(error.involved_nodes)};
41}
42
43template <typename T, std::ranges::input_range R>
44void AppendTargetRange(std::vector<T>& targets, R&& range) {
45 if constexpr (requires { targets.append_range(range); }) {
46 targets.append_range(std::forward<R>(range));
47 } else {
48 targets.insert(targets.end(), std::ranges::begin(range),
49 std::ranges::end(range));
50 }
51}
52
53} // namespace
54
55void Schedule::Run(World& world, Executor& executor) {
56 HELIOS_ASSERT(!is_dirty_,
57 "Schedule::Run called but schedule is dirty! Call "
58 "Schedule::Build first.");
59 HELIOS_ASSERT(plan_.has_value(),
60 "Schedule::Run called but no plan exists! Call Build first.");
63 "Schedule '{}' still has unapplied commands or messages from a prior "
64 "Run(). Call ApplyDeferred(world) or use RunAndWait() before running "
65 "again.",
66 name_);
67
68 HELIOS_ECS_PROFILE_SCOPE();
69 HELIOS_ECS_PROFILE_ZONE_NAME(
70 std::format("helios::ecs::Schedule::Run{{name: {}}}", name_));
71 HELIOS_ECS_PROFILE_ZONE_VALUE(plan_->execution_order.size());
72
73 for (auto& entry : system_entries_) {
74 entry.storage.local_data.ResetArena();
75 }
76
77 executor.Execute(*this, world);
78}
79
80void Schedule::RunAndWait(World& world, Executor& executor) {
81 HELIOS_ASSERT(!is_dirty_,
82 "Schedule::RunAndWait called but schedule is dirty! Call "
83 "Schedule::Build first.");
84 HELIOS_ASSERT(plan_.has_value(),
85 "Schedule::RunAndWait called but no plan exists! Call "
86 "Schedule::Build first.");
89 "Schedule '{}' still has unapplied commands or messages from a prior "
90 "Run(). Call ApplyDeferred(world) before RunAndWait().",
91 name_);
92
93 HELIOS_ECS_PROFILE_SCOPE();
94 HELIOS_ECS_PROFILE_ZONE_NAME(
95 std::format("helios::ecs::Schedule::RunAndWait{{name: {}}}", name_));
96 HELIOS_ECS_PROFILE_ZONE_VALUE(plan_->execution_order.size());
97
98 for (auto& entry : system_entries_) {
99 entry.storage.local_data.ResetArena();
100 }
101
102 executor.ExecuteAndWait(*this, world);
103 ApplyDeferred(world);
104}
105
107 HELIOS_ECS_PROFILE_SCOPE();
108 HELIOS_ECS_PROFILE_ZONE_NAME(
109 std::format("helios::ecs::Schedule::ApplyDeferred{{name: {}}}", name_));
110
111 world.Flush();
112 for (auto& entry : system_entries_) {
113 entry.storage.local_data.Update(world);
114 }
115}
116
118 if (!is_dirty_ && plan_.has_value()) {
119 return {};
120 }
121
122 HELIOS_ECS_PROFILE_SCOPE();
123 HELIOS_ECS_PROFILE_ZONE_NAME(
124 std::format("helios::ecs::Schedule::Build{{name: {}}}", name_));
125 HELIOS_ECS_PROFILE_ZONE_VALUE(system_entries_.size());
126
127 if (auto result = ResolveSetReferences(); !result) [[unlikely]] {
128 return std::unexpected(std::move(result.error()));
129 }
130
131 auto dag_result = BuildDag();
132 if (!dag_result) [[unlikely]] {
133 return std::unexpected(std::move(dag_result.error()));
134 }
135
136 auto sort_result = dag_result->Sort();
137 if (!sort_result) [[unlikely]] {
138 return std::unexpected(DagToScheduleError(sort_result.error()));
139 }
140
141 WarnAmbiguousAccess(*dag_result);
142
143 CompiledPlan plan;
144 plan.execution_order = std::move(*sort_result);
145
146 for (size_t i = 0; i < system_entries_.size(); ++i) {
147 plan.system_id_to_entry_idx[system_entries_[i].storage.id.id] = i;
148 }
149
150 BuildConflictData(plan);
151
152 plan_ = std::move(plan);
153
154 MergeRunConditions(conditions_cache_, plan_->execution_order);
155
156 is_dirty_ = false;
157 return {};
158}
159
161 system_entries_.clear();
162 sets_.clear();
163 conditions_cache_.clear();
164 plan_.reset();
165 next_anonymous_group_id_ = 1;
166 is_dirty_ = true;
167 ++generation_;
168}
169
170size_t Schedule::AddEntry(SystemStorage&& storage) {
171 const size_t index = system_entries_.size();
172
173 for (const auto& entry : system_entries_) {
174 if (entry.storage.id == storage.id) {
175 storage.id = SystemId::From(std::format("{}#{}", storage.name, index));
176 break;
177 }
178 }
179
180 system_entries_.push_back(SystemEntry{
181 .storage = std::move(storage), .metadata = {}, .is_sync_point = false});
182 MarkDirty();
183 return index;
184}
185
186auto Schedule::BuildDag() -> ScheduleResult<Dag> {
187 Dag dag;
188
189 for (size_t i = 0; i < system_entries_.size(); ++i) {
190 const SystemId id = system_entries_[i].storage.id;
191 dag.AddNode(id);
192 }
193
194 for (size_t i = 0; i < system_entries_.size(); ++i) {
195 const auto& entry = system_entries_[i];
196 const SystemId from_id = entry.storage.id;
197
198 for (const SystemId target : entry.metadata.before_targets) {
199 if (auto result = dag.AddEdge(from_id, target); !result) [[unlikely]] {
200 return std::unexpected(DagToScheduleError(std::move(result.error())));
201 }
202 }
203
204 for (const SystemId target : entry.metadata.after_targets) {
205 if (auto result = dag.AddEdge(target, from_id); !result) [[unlikely]] {
206 return std::unexpected(DagToScheduleError(std::move(result.error())));
207 }
208 }
209 }
210
211 return dag;
212}
213
214auto Schedule::ResolveSetReferences() -> ScheduleResult<void> {
215 std::unordered_map<size_t, std::vector<SystemId>> set_members;
216 for (const auto& entry : system_entries_) {
217 for (const SystemSetId set_id : entry.metadata.member_of_sets) {
218 set_members[set_id.id].push_back(entry.storage.id);
219 }
220 }
221
222 const auto resolve_set =
223 [this, &set_members](
224 SystemSetId set_id) -> std::optional<std::span<const SystemId>> {
225 if (!sets_.contains(set_id.id)) [[unlikely]] {
226 return std::nullopt;
227 }
228
229 const auto it = set_members.find(set_id.id);
230 if (it == set_members.end()) {
231 return std::span<const SystemId>{};
232 }
233 return std::span<const SystemId>{it->second};
234 };
235
236 for (auto& entry : system_entries_) {
237 for (const SystemSetId set_id : entry.metadata.before_set_targets) {
238 const auto members = resolve_set(set_id);
239 if (!members) [[unlikely]] {
240 return std::unexpected(ScheduleError{
242 .message = std::format("Unknown set referenced by system '{}'",
243 entry.storage.name),
244 .involved_systems = {entry.storage.id}});
245 }
246 AppendTargetRange(entry.metadata.before_targets, *members);
247 }
248 entry.metadata.before_set_targets.clear();
249
250 for (const SystemSetId set_id : entry.metadata.after_set_targets) {
251 const auto members = resolve_set(set_id);
252 if (!members) [[unlikely]] {
253 return std::unexpected(ScheduleError{
255 .message = std::format("Unknown set referenced by system '{}'",
256 entry.storage.name),
257 .involved_systems = {entry.storage.id}});
258 }
259 AppendTargetRange(entry.metadata.after_targets, *members);
260 }
261 entry.metadata.after_set_targets.clear();
262 }
263
264 for (auto& entry : system_entries_) {
265 for (const SystemSetId set_id : entry.metadata.member_of_sets) {
266 const auto set_it = sets_.find(set_id.id);
267 if (set_it == sets_.end()) {
268 continue;
269 }
270
271 const auto& before = set_it->second.BeforeTargets();
272 const auto& after = set_it->second.AfterTargets();
273 const auto& before_sets = set_it->second.BeforeSetTargets();
274 const auto& after_sets = set_it->second.AfterSetTargets();
275
276 AppendTargetRange(entry.metadata.before_targets, before);
277 AppendTargetRange(entry.metadata.after_targets, after);
278 AppendTargetRange(entry.metadata.before_set_targets, before_sets);
279 AppendTargetRange(entry.metadata.after_set_targets, after_sets);
280 }
281 }
282
283 for (auto& entry : system_entries_) {
284 for (const SystemSetId set_id : entry.metadata.before_set_targets) {
285 const auto members = resolve_set(set_id);
286 if (!members) [[unlikely]] {
287 return std::unexpected(ScheduleError{
289 .message = std::format("Unknown set referenced by system '{}'",
290 entry.storage.name),
291 .involved_systems = {entry.storage.id}});
292 }
293 AppendTargetRange(entry.metadata.before_targets, *members);
294 }
295 entry.metadata.before_set_targets.clear();
296
297 for (const SystemSetId set_id : entry.metadata.after_set_targets) {
298 const auto members = resolve_set(set_id);
299 if (!members) [[unlikely]] {
300 return std::unexpected(ScheduleError{
302 .message = std::format("Unknown set referenced by system '{}'",
303 entry.storage.name),
304 .involved_systems = {entry.storage.id}});
305 }
306 AppendTargetRange(entry.metadata.after_targets, *members);
307 }
308 entry.metadata.after_set_targets.clear();
309 }
310
311 ApplySequenceOrdering(set_members);
312
313 return {};
314}
315
316void Schedule::ApplySequenceOrdering(
317 const std::unordered_map<size_t, std::vector<SystemId>>& set_members) {
318 for (const auto& [set_id, set] : sets_) {
319 if (!set.IsSequence()) {
320 continue;
321 }
322
323 const auto members_it = set_members.find(set_id);
324 if (members_it == set_members.end() || members_it->second.size() < 2) {
325 continue;
326 }
327
328 const auto& members = members_it->second;
329 for (size_t i = 1; i < members.size(); ++i) {
330 for (auto& entry : system_entries_) {
331 if (entry.storage.id == members[i]) {
332 entry.metadata.AddAfter(members[i - 1]);
333 break;
334 }
335 }
336 }
337 }
338}
339
340void Schedule::WarnAmbiguousAccess(const Dag& dag) {
341 for (size_t i = 0; i < system_entries_.size(); ++i) {
342 for (size_t j = i + 1; j < system_entries_.size(); ++j) {
343 const auto& policy_i = system_entries_[i].storage.access_policy;
344 const auto& policy_j = system_entries_[j].storage.access_policy;
345
346 if (!policy_i.ConflictsWith(policy_j)) {
347 continue;
348 }
349
350 const SystemId id_i = system_entries_[i].storage.id;
351 const SystemId id_j = system_entries_[j].storage.id;
352
353 if (dag.Reachable(id_i, id_j) || dag.Reachable(id_j, id_i)) {
354 continue;
355 }
356
357 log::Warn("Ambiguous access between '{}' and '{}'!",
358 system_entries_[i].storage.name,
359 system_entries_[j].storage.name);
360 }
361 }
362}
363
364void Schedule::BuildConflictData(CompiledPlan& plan) {
365 const size_t size = plan.execution_order.size();
366
367 if (size <= 64) {
368 plan.conflicts.bitmask.assign(size, 0);
369
370 for (size_t i = 0; i < size; ++i) {
371 const SystemId id_i = plan.execution_order[i];
372 const auto it_i = plan.system_id_to_entry_idx.find(id_i.id);
373 if (it_i == plan.system_id_to_entry_idx.end()) {
374 continue;
375 }
376
377 const auto& policy_i =
378 system_entries_[it_i->second].storage.access_policy;
379
380 for (size_t j = i + 1; j < size; ++j) {
381 const SystemId id_j = plan.execution_order[j];
382 const auto it_j = plan.system_id_to_entry_idx.find(id_j.id);
383 if (it_j == plan.system_id_to_entry_idx.end()) {
384 continue;
385 }
386
387 const auto& policy_j =
388 system_entries_[it_j->second].storage.access_policy;
389
390 if (policy_i.ConflictsWith(policy_j)) {
391 plan.conflicts.bitmask[i] |= (1ULL << j);
392 plan.conflicts.bitmask[j] |= (1ULL << i);
393 }
394 }
395 }
396 } else {
397 plan.conflicts.matrix.assign(size, std::vector<bool>(size, false));
398
399 for (size_t i = 0; i < size; ++i) {
400 const SystemId id_i = plan.execution_order[i];
401 const auto it_i = plan.system_id_to_entry_idx.find(id_i.id);
402 if (it_i == plan.system_id_to_entry_idx.end()) {
403 continue;
404 }
405
406 const auto& policy_i =
407 system_entries_[it_i->second].storage.access_policy;
408
409 for (size_t j = i + 1; j < size; ++j) {
410 const SystemId id_j = plan.execution_order[j];
411 const auto it_j = plan.system_id_to_entry_idx.find(id_j.id);
412 if (it_j == plan.system_id_to_entry_idx.end()) {
413 continue;
414 }
415
416 const auto& policy_j =
417 system_entries_[it_j->second].storage.access_policy;
418
419 const bool conflict = policy_i.ConflictsWith(policy_j);
420 plan.conflicts.matrix[i][j] = conflict;
421 plan.conflicts.matrix[j][i] = conflict;
422 }
423 }
424 }
425}
426
427void Schedule::MergeRunConditions(
428 std::vector<std::vector<RunConditionStorage*>>& out_conditions,
429 const std::vector<SystemId>& execution_order) {
430 out_conditions.clear();
431 out_conditions.resize(execution_order.size());
432
433 HELIOS_ASSERT(plan_.has_value(),
434 "MergeRunConditions: schedule has no compiled plan!");
435
436 for (size_t exec_idx = 0; exec_idx < execution_order.size(); ++exec_idx) {
437 const SystemId id = execution_order[exec_idx];
438 const auto it = plan_->system_id_to_entry_idx.find(id.id);
439 if (it == plan_->system_id_to_entry_idx.end()) {
440 continue;
441 }
442
443 auto& entry = system_entries_[it->second];
444 auto& conditions = out_conditions[exec_idx];
445
446 for (auto& condition : entry.metadata.conditions) {
447 conditions.push_back(&condition);
448 }
449
450 for (const SystemSetId set_id : entry.metadata.member_of_sets) {
451 const auto set_it = sets_.find(set_id.id);
452 if (set_it == sets_.end()) {
453 continue;
454 }
455
456 for (auto& condition : set_it->second.Conditions()) {
457 conditions.push_back(const_cast<RunConditionStorage*>(&condition));
458 }
459 }
460 }
461}
462
463} // namespace helios::ecs
#define HELIOS_ASSERT(condition,...)
Assertion macro that aborts execution in debug builds.
Definition assert.hpp:259
Directed acyclic graph used to compute the topological execution order of systems within a schedule.
Definition dag.hpp:33
Abstract interface for schedule executors.
Definition executor.hpp:27
virtual void ExecuteAndWait(Schedule &schedule, World &world)
Executes all systems in the schedule and blocks until completion.
Definition executor.hpp:55
virtual void Execute(Schedule &schedule, World &world)=0
Submits all systems in the schedule for execution.
auto Build() -> ScheduleResult< void >
Compiles the schedule into an executable plan.
Definition schedule.cpp:117
void RunAndWait(World &world, Executor &executor)
Executes the compiled schedule and blocks until completion.
Definition schedule.cpp:80
void ApplyDeferred(World &world)
Flushes world state and applies all pending system-local data.
Definition schedule.cpp:106
bool HasPendingLocalData() const noexcept
Checks whether any system still has unapplied local commands or messages.
Definition schedule.hpp:381
void Run(World &world, Executor &executor)
Submits the compiled schedule for execution using the provided executor.
Definition schedule.cpp:55
void Clear()
Clears all systems, sets, and compiled state from this schedule.
Definition schedule.cpp:160
The World class manages entities with their components and systems.
Definition world.hpp:39
void Flush()
Flushes entity reservations and executes pending commands.
Definition world.hpp:901
constexpr ScheduleErrorKind DagToScheduleErrorKind(DagErrorKind kind) noexcept
Definition schedule.cpp:24
constexpr ScheduleError DagToScheduleError(DagError error) noexcept
Definition schedule.cpp:36
void AppendTargetRange(std::vector< T > &targets, R &&range)
Definition schedule.cpp:44
std::expected< T, ScheduleError > ScheduleResult
Result type for schedule operations that can fail.
Definition schedule.hpp:173
ScheduleErrorKind
Kind of error produced during schedule compilation.
Definition schedule.hpp:156
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
Id for systems.
Definition system.hpp:146
static constexpr SystemId From() noexcept
Creates system id from a system type.
Definition system.hpp:182
Identifier for a system set.
Storage for a system.