Gaia-ECS v1.0.0
A simple and powerful entity component system
Loading...
Searching...
No Matches
sched.h
1#pragma once
2
3#include "gaia/config/config.h"
4
5#include <cstdint>
6
7#include "gaia/cnt/darray.h"
8#include "gaia/mem/smallblock_allocator.h"
9#include "gaia/mt/jobcommon.h"
10#include "gaia/mt/jobhandle.h"
11#include "gaia/mt/threadpool.h"
12
13namespace gaia {
14 namespace ecs {
15 enum class QueryExecType : uint32_t;
16
19 struct SchedToken {
21 uintptr_t value[2]{};
22 };
23
25 enum class SchedFlags : uint8_t {
27 Default = 0,
29 Background = 0x01
30 };
31
36 GAIA_NODISCARD inline bool sched_flags_has(SchedFlags flags, SchedFlags flag) {
37 return ((uint8_t)flags & (uint8_t)flag) != 0U;
38 }
39
43 void* pCtx = nullptr;
45 void (*invoke)(void* pCtx) = nullptr;
47 QueryExecType execType{};
49 SchedFlags flags = SchedFlags::Default;
50 };
51
53 struct SchedParDesc {
55 void* pCtx = nullptr;
57 void (*invoke)(void* pCtx, uint32_t idxStart, uint32_t idxEnd) = nullptr;
59 uint32_t itemCount = 0;
61 uint32_t groupSize = 0;
63 QueryExecType execType{};
65 SchedFlags flags = SchedFlags::Default;
66 };
67
71 struct Sched {
73 void* pCtx = nullptr;
78 SchedToken (*sched)(void* pCtx, const SchedTaskDesc* pDesc) = nullptr;
83 SchedToken (*sched_par)(void* pCtx, const SchedParDesc* pDesc) = nullptr;
88 SchedToken (*add)(void* pCtx, const SchedTaskDesc* pDesc) = nullptr;
93 SchedToken (*add_par)(void* pCtx, const SchedParDesc* pDesc) = nullptr;
97 void (*submit)(void* pCtx, SchedToken token) = nullptr;
102 void (*dep)(void* pCtx, SchedToken tokenFirst, SchedToken tokenSecond) = nullptr;
106 void (*wait)(void* pCtx, SchedToken token) = nullptr;
110 void (*del)(void* pCtx, SchedToken token) = nullptr;
111 };
112
118 class SchedJob {
119 Sched m_sched;
120 SchedToken m_token{};
121 void* m_pCleanupCtx = nullptr;
122 void (*m_cleanup)(void* pCtx) = nullptr;
123 bool m_valid = false;
124 bool m_submitted = false;
125 bool m_waited = false;
126
127 void cleanup() {
128 if (m_cleanup != nullptr) {
129 m_cleanup(m_pCleanupCtx);
130 m_cleanup = nullptr;
131 m_pCleanupCtx = nullptr;
132 }
133 }
134
135 GAIA_NODISCARD static bool same_sched(const Sched& a, const Sched& b) {
136 return a.pCtx == b.pCtx && a.sched == b.sched && a.sched_par == b.sched_par && a.add == b.add &&
137 a.add_par == b.add_par && a.submit == b.submit && a.dep == b.dep && a.wait == b.wait && a.del == b.del;
138 }
139
140 public:
141 SchedJob() = default;
142
148 SchedJob(Sched sched, SchedToken token, bool submitted, void* pCleanupCtx, void (*cleanup)(void* pCtx)):
149 m_sched(sched), m_token(token), m_pCleanupCtx(pCleanupCtx), m_cleanup(cleanup), m_valid(true),
150 m_submitted(submitted) {}
151
154 del();
155 }
156
157 SchedJob(const SchedJob&) = delete;
158 SchedJob& operator=(const SchedJob&) = delete;
159
162 SchedJob(SchedJob&& other) noexcept:
163 m_sched(other.m_sched), m_token(other.m_token), m_pCleanupCtx(other.m_pCleanupCtx),
164 m_cleanup(other.m_cleanup), m_valid(other.m_valid), m_submitted(other.m_submitted), m_waited(other.m_waited) {
165 other.m_valid = false;
166 other.m_cleanup = nullptr;
167 other.m_pCleanupCtx = nullptr;
168 }
169
173 SchedJob& operator=(SchedJob&& other) noexcept {
174 if (this == &other)
175 return *this;
176 del();
177 m_sched = other.m_sched;
178 m_token = other.m_token;
179 m_pCleanupCtx = other.m_pCleanupCtx;
180 m_cleanup = other.m_cleanup;
181 m_valid = other.m_valid;
182 m_submitted = other.m_submitted;
183 m_waited = other.m_waited;
184 other.m_valid = false;
185 other.m_cleanup = nullptr;
186 other.m_pCleanupCtx = nullptr;
187 return *this;
188 }
189
192 GAIA_NODISCARD bool valid() const {
193 return m_valid;
194 }
195
198 GAIA_NODISCARD SchedToken token() const {
199 return m_token;
200 }
201
203 void submit();
214 void dep(const SchedJob& jobFirst);
217 void wait();
219 void del();
220 };
221
223 namespace detail {
224 inline mt::JobPriority exec_prio(QueryExecType execType) {
225 // QueryExecType::ParallelEff is encoded as value 3. Keep the scheduler bridge independent
226 // from the enum definition point so this header stays C-like and forward-declarable.
227 return (uint32_t)execType == 3U ? mt::JobPriority::Low : mt::JobPriority::High;
228 }
229
230 inline bool sched_flags_background(SchedFlags flags) {
231 return sched_flags_has(flags, SchedFlags::Background);
232 }
233
234 inline mt::JobCreationFlags job_creation_flags(SchedFlags flags) {
235 uint8_t jobFlags = (uint8_t)mt::JobCreationFlags::ManualDelete;
236 if (sched_flags_background(flags))
237 jobFlags |= (uint8_t)mt::JobCreationFlags::Background;
238 return (mt::JobCreationFlags)jobFlags;
239 }
240
241 enum class SchedTokenKind : uint32_t { None, Single, Parallel };
242
243 struct SchedTokenDefData {
244 cnt::darray<mt::JobHandle> handles;
245 SchedTokenKind kind = SchedTokenKind::None;
246 bool submitted = false;
247
248 GAIA_USE_SMALLBLOCK(SchedTokenDefData)
249 };
250
251 inline SchedToken make_sched_token(SchedTokenDefData* pData) {
252 SchedToken token{};
253 token.value[0] = (uintptr_t)pData;
254 return token;
255 }
256
257 inline SchedTokenDefData* sched_token_data(SchedToken token) {
258 return reinterpret_cast<SchedTokenDefData*>(token.value[0]);
259 }
260
261 inline SchedToken add_one_def([[maybe_unused]] void* pCtx, const SchedTaskDesc* pDesc) {
262 GAIA_ASSERT(pDesc != nullptr);
263 GAIA_ASSERT(pDesc->invoke != nullptr);
264 if (pDesc == nullptr || pDesc->invoke == nullptr)
265 return {};
266
267 auto* pData = new SchedTokenDefData();
268 pData->kind = SchedTokenKind::Single;
269 pData->handles.resize(1);
270
271 auto func = [pCtx = pDesc->pCtx, invoke = pDesc->invoke]() {
272 invoke(pCtx);
273 };
274 static_assert(mt::JobFunc::stores_inline<decltype(func)>(), "Scheduler jobs must not allocate");
275
276 mt::Job job;
277 job.priority = exec_prio(pDesc->execType);
278 job.flags = job_creation_flags(pDesc->flags);
279 job.func = GAIA_MOV(func);
280
281 pData->handles[0] = mt::ThreadPool::get().add(GAIA_MOV(job));
282 return make_sched_token(pData);
283 }
284
285 inline SchedToken sched_one_def([[maybe_unused]] void* pCtx, const SchedTaskDesc* pDesc) {
286 auto token = add_one_def(pCtx, pDesc);
287 auto* pData = sched_token_data(token);
288 if (pData != nullptr) {
289 mt::ThreadPool::get().submit(pData->handles[0]);
290 pData->submitted = true;
291 }
292 return token;
293 }
294
295 inline SchedToken add_par_def([[maybe_unused]] void* pCtx, const SchedParDesc* pDesc) {
296 GAIA_ASSERT(pDesc != nullptr);
297 GAIA_ASSERT(pDesc->invoke != nullptr);
298 if (pDesc == nullptr || pDesc->invoke == nullptr || pDesc->itemCount == 0)
299 return {};
300
301 auto& tp = mt::ThreadPool::get();
302 const auto prio = exec_prio(pDesc->execType);
303 uint32_t groupSize = pDesc->groupSize;
304 if (groupSize == 0) {
305 const bool background = sched_flags_background(pDesc->flags);
306 const auto workers =
307 background ? core::get_max(1U, tp.background_workers()) : core::get_max(1U, tp.workers() + 1U);
308 groupSize = (pDesc->itemCount + workers - 1) / workers;
309 constexpr uint32_t maxUnitsOfWorkPerGroup = 8;
310 groupSize = groupSize / maxUnitsOfWorkPerGroup;
311 if (groupSize == 0)
312 groupSize = 1;
313 }
314
315 const auto jobs = (pDesc->itemCount + groupSize - 1) / groupSize;
316 auto* pData = new SchedTokenDefData();
317 pData->kind = SchedTokenKind::Parallel;
318 pData->handles.resize(jobs + 1);
319
320 for (uint32_t jobIndex = 0; jobIndex < jobs; ++jobIndex) {
321 const uint32_t idxStart = jobIndex * groupSize;
322 const uint32_t idxEnd = core::get_min(idxStart + groupSize, pDesc->itemCount);
323
324 auto func = [pCtx = pDesc->pCtx, invoke = pDesc->invoke, idxStart, idxEnd]() {
325 invoke(pCtx, idxStart, idxEnd);
326 };
327 static_assert(mt::JobFunc::stores_inline<decltype(func)>(), "Scheduler jobs must not allocate");
328
329 mt::Job job;
330 job.priority = prio;
331 job.flags = job_creation_flags(pDesc->flags);
332 job.func = GAIA_MOV(func);
333 pData->handles[jobIndex] = tp.add(GAIA_MOV(job));
334 }
335 {
336 mt::Job syncJob;
337 syncJob.priority = prio;
338 syncJob.flags = job_creation_flags(pDesc->flags);
339 pData->handles[jobs] = tp.add(GAIA_MOV(syncJob));
340 }
341 tp.dep(std::span(pData->handles.data(), jobs), pData->handles[jobs]);
342 return make_sched_token(pData);
343 }
344
345 inline SchedToken sched_par_def([[maybe_unused]] void* pCtx, const SchedParDesc* pDesc) {
346 auto token = add_par_def(pCtx, pDesc);
347 auto* pData = sched_token_data(token);
348 if (pData != nullptr) {
349 mt::ThreadPool::get().submit(std::span(pData->handles.data(), pData->handles.size()));
350 pData->submitted = true;
351 }
352 return token;
353 }
354
355 inline void sched_submit_def([[maybe_unused]] void* pCtx, SchedToken token) {
356 auto* pData = sched_token_data(token);
357 if (pData == nullptr || pData->submitted)
358 return;
359 auto& tp = mt::ThreadPool::get();
360 tp.submit(std::span(pData->handles.data(), pData->handles.size()));
361 pData->submitted = true;
362 }
363
364 inline void sched_dep_def([[maybe_unused]] void* pCtx, SchedToken tokenFirst, SchedToken tokenSecond) {
365 auto* pFirst = sched_token_data(tokenFirst);
366 auto* pSecond = sched_token_data(tokenSecond);
367 if (pFirst == nullptr || pSecond == nullptr || pFirst->handles.empty() || pSecond->handles.empty())
368 return;
369 auto& tp = mt::ThreadPool::get();
370 const auto firstDone = pFirst->handles.back();
371 if (pSecond->kind == SchedTokenKind::Parallel && pSecond->handles.size() > 1) {
372 const auto childCount = (uint32_t)pSecond->handles.size() - 1;
373 for (uint32_t i = 0; i < childCount; ++i)
374 tp.dep(firstDone, pSecond->handles[i]);
375 return;
376 }
377 tp.dep(firstDone, pSecond->handles.back());
378 }
379
380 inline void sched_wait_def([[maybe_unused]] void* pCtx, SchedToken token) {
381 auto* pData = sched_token_data(token);
382 if (pData == nullptr || pData->handles.empty())
383 return;
384 mt::ThreadPool::get().wait(pData->handles.back());
385 }
386
387 inline void sched_del_def([[maybe_unused]] void* pCtx, [[maybe_unused]] SchedToken token) {
388 auto* pData = sched_token_data(token);
389 if (pData == nullptr)
390 return;
391 auto& tp = mt::ThreadPool::get();
392 for (auto handle: pData->handles) {
393 if (handle != mt::JobNull)
394 tp.del(handle);
395 }
396 delete pData;
397 }
398 } // namespace detail
400
403 GAIA_NODISCARD inline const Sched& sched_def() {
404 static const Sched sched = [] {
405 Sched b{};
406 b.sched = &detail::sched_one_def;
407 b.sched_par = &detail::sched_par_def;
408 b.add = &detail::add_one_def;
409 b.add_par = &detail::add_par_def;
410 b.submit = &detail::sched_submit_def;
411 b.dep = &detail::sched_dep_def;
412 b.wait = &detail::sched_wait_def;
413 b.del = &detail::sched_del_def;
414 return b;
415 }();
416 return sched;
417 }
418
422 GAIA_NODISCARD inline const Sched& sched_resolve(const Sched& sched) {
423 if (sched.sched == nullptr && sched.sched_par == nullptr && sched.add == nullptr && sched.add_par == nullptr &&
424 sched.submit == nullptr && sched.dep == nullptr && sched.wait == nullptr && sched.del == nullptr)
425 return sched_def();
426 return sched;
427 }
428
433 GAIA_NODISCARD inline SchedToken sched_one(const Sched& sched, const SchedTaskDesc& desc) {
434 const auto& resolved = sched_resolve(sched);
435 if (resolved.sched != nullptr)
436 return resolved.sched(resolved.pCtx, &desc);
437 GAIA_ASSERT(resolved.add != nullptr);
438 GAIA_ASSERT(resolved.submit != nullptr);
439 const auto token = resolved.add != nullptr ? resolved.add(resolved.pCtx, &desc) : SchedToken{};
440 if (resolved.submit != nullptr)
441 resolved.submit(resolved.pCtx, token);
442 return token;
443 }
444
452 GAIA_NODISCARD inline SchedJob
453 sched_add(const Sched& sched, const SchedTaskDesc& desc, void* pCleanupCtx, void (*cleanup)(void* pCtx)) {
454 const auto& resolved = sched_resolve(sched);
455 if (resolved.add != nullptr) {
456 if (resolved.submit == nullptr || resolved.wait == nullptr || resolved.del == nullptr) {
457 GAIA_ASSERT(false);
458 if (cleanup != nullptr)
459 cleanup(pCleanupCtx);
460 return {};
461 }
462 return SchedJob(resolved, resolved.add(resolved.pCtx, &desc), false, pCleanupCtx, cleanup);
463 }
464
465 if (resolved.sched == nullptr || resolved.wait == nullptr || resolved.del == nullptr) {
466 GAIA_ASSERT(false);
467 if (cleanup != nullptr)
468 cleanup(pCleanupCtx);
469 return {};
470 }
471 return SchedJob(resolved, resolved.sched(resolved.pCtx, &desc), true, pCleanupCtx, cleanup);
472 }
473
478 GAIA_NODISCARD inline SchedToken sched_par(const Sched& sched, const SchedParDesc& desc) {
479 const auto& resolved = sched_resolve(sched);
480 if (resolved.sched_par != nullptr)
481 return resolved.sched_par(resolved.pCtx, &desc);
482 GAIA_ASSERT(resolved.add_par != nullptr);
483 GAIA_ASSERT(resolved.submit != nullptr);
484 const auto token = resolved.add_par != nullptr ? resolved.add_par(resolved.pCtx, &desc) : SchedToken{};
485 if (resolved.submit != nullptr)
486 resolved.submit(resolved.pCtx, token);
487 return token;
488 }
489
497 GAIA_NODISCARD inline SchedJob
498 sched_add_par(const Sched& sched, const SchedParDesc& desc, void* pCleanupCtx, void (*cleanup)(void* pCtx)) {
499 const auto& resolved = sched_resolve(sched);
500 if (resolved.add_par != nullptr) {
501 if (resolved.submit == nullptr || resolved.wait == nullptr || resolved.del == nullptr) {
502 GAIA_ASSERT(false);
503 if (cleanup != nullptr)
504 cleanup(pCleanupCtx);
505 return {};
506 }
507 return SchedJob(resolved, resolved.add_par(resolved.pCtx, &desc), false, pCleanupCtx, cleanup);
508 }
509
510 if (resolved.sched_par == nullptr || resolved.wait == nullptr || resolved.del == nullptr) {
511 GAIA_ASSERT(false);
512 if (cleanup != nullptr)
513 cleanup(pCleanupCtx);
514 return {};
515 }
516 return SchedJob(resolved, resolved.sched_par(resolved.pCtx, &desc), true, pCleanupCtx, cleanup);
517 }
518
522 inline void sched_submit(const Sched& sched, SchedToken token) {
523 const auto& resolved = sched_resolve(sched);
524 if (resolved.submit != nullptr)
525 resolved.submit(resolved.pCtx, token);
526 }
527
532 inline void sched_dep(const Sched& sched, SchedToken tokenFirst, SchedToken tokenSecond) {
533 const auto& resolved = sched_resolve(sched);
534 if (resolved.dep != nullptr)
535 resolved.dep(resolved.pCtx, tokenFirst, tokenSecond);
536 }
537
541 inline void sched_wait(const Sched& sched, SchedToken token) {
542 const auto& resolved = sched_resolve(sched);
543 if (resolved.wait != nullptr)
544 resolved.wait(resolved.pCtx, token);
545 }
546
550 inline void sched_del(const Sched& sched, SchedToken token) {
551 const auto& resolved = sched_resolve(sched);
552 if (resolved.del != nullptr)
553 resolved.del(resolved.pCtx, token);
554 }
555
556 inline void SchedJob::submit() {
557 if (!m_valid || m_submitted)
558 return;
559 GAIA_ASSERT(m_sched.submit != nullptr);
560 if (m_sched.submit == nullptr)
561 return;
562 sched_submit(m_sched, m_token);
563 m_submitted = true;
564 }
565
566 inline void SchedJob::dep(const SchedJob& jobFirst) {
567 if (!m_valid || !jobFirst.m_valid)
568 return;
569 GAIA_ASSERT(same_sched(m_sched, jobFirst.m_sched));
570 GAIA_ASSERT(!m_submitted && !jobFirst.m_submitted);
571 if (!same_sched(m_sched, jobFirst.m_sched) || m_submitted || jobFirst.m_submitted)
572 return;
573 sched_dep(m_sched, jobFirst.m_token, m_token);
574 }
575
576 inline void SchedJob::wait() {
577 if (!m_valid || m_waited)
578 return;
579 GAIA_ASSERT(m_submitted);
580 if (!m_submitted)
581 return;
582 sched_wait(m_sched, m_token);
583 m_waited = true;
584 cleanup();
585 }
586
587 inline void SchedJob::del() {
588 if (!m_valid)
589 return;
590 if (m_submitted && !m_waited)
591 wait();
592 sched_del(m_sched, m_token);
593 cleanup();
594 m_valid = false;
595 m_submitted = false;
596 m_waited = false;
597 }
598 } // namespace ecs
599} // namespace gaia
Move-only wrapper for scheduler-owned ECS work.
Definition sched.h:118
GAIA_NODISCARD bool valid() const
Returns true if this wrapper owns scheduler work.
Definition sched.h:192
void submit()
Submits the added work if it has not been submitted yet.
Definition sched.h:556
SchedJob & operator=(SchedJob &&other) noexcept
Move-assigns a scheduler job wrapper.
Definition sched.h:173
GAIA_NODISCARD SchedToken token() const
Returns the opaque scheduler token.
Definition sched.h:198
void del()
Deletes scheduler resources and runs wrapper cleanup.
Definition sched.h:587
SchedJob(Sched sched, SchedToken token, bool submitted, void *pCleanupCtx, void(*cleanup)(void *pCtx))
Definition sched.h:148
void wait()
Waits for submitted work to complete.
Definition sched.h:576
~SchedJob()
Waits for and deletes a still-owned token.
Definition sched.h:153
void dep(const SchedJob &jobFirst)
Adds a dependency edge so this job runs after jobFirst.
Definition sched.h:566
SchedJob(SchedJob &&other) noexcept
Move-constructs a scheduler job wrapper.
Definition sched.h:162
JobHandle add(TJob job)
Creates a threadpool job from job.
Definition threadpool.h:391
static ThreadPool & get()
Returns the process-wide thread-pool instance.
Definition threadpool.h:168
void submit(std::span< JobHandle > jobHandles)
Pushes jobHandles into the internal queue so worker threads can pick them up and execute them....
Definition threadpool.h:522
void wait(JobHandle jobHandle)
Wait until a job associated with the jobHandle finishes executing. Cleans up any job allocations and ...
Definition threadpool.h:863
Description of a parallel-for submission to a scheduler.
Definition sched.h:53
void * pCtx
Opaque callback context forwarded to invoke().
Definition sched.h:55
void(* invoke)(void *pCtx, uint32_t idxStart, uint32_t idxEnd)
Parallel-for entry point receiving a half-open item range [idxStart, idxEnd).
Definition sched.h:57
uint32_t groupSize
Preferred group size. A value of 0 lets the scheduler choose.
Definition sched.h:61
SchedFlags flags
Scheduler flags describing non-default execution requirements.
Definition sched.h:65
QueryExecType execType
Execution hint selected by the scheduler caller.
Definition sched.h:63
uint32_t itemCount
Total number of items to process.
Definition sched.h:59
Description of a single task submitted to a scheduler.
Definition sched.h:41
SchedFlags flags
Scheduler flags describing non-default execution requirements.
Definition sched.h:49
void(* invoke)(void *pCtx)
Task entry point.
Definition sched.h:45
QueryExecType execType
Execution hint selected by the scheduler caller.
Definition sched.h:47
void * pCtx
Opaque callback context forwarded to invoke().
Definition sched.h:43
Opaque synchronization token returned by a scheduler. The scheduler owns the meaning of the payload.
Definition sched.h:19
uintptr_t value[2]
Scheduler-defined token payload.
Definition sched.h:21
Scheduler descriptor used by ECS runtime code. All callbacks may be null when the descriptor is only ...
Definition sched.h:71
SchedToken(* add_par)(void *pCtx, const SchedParDesc *pDesc)
Adds a parallel-for workload without submitting it for execution.
Definition sched.h:93
void * pCtx
Opaque scheduler-owned context passed back to every callback.
Definition sched.h:73
void(* dep)(void *pCtx, SchedToken tokenFirst, SchedToken tokenSecond)
Adds a dependency edge so tokenSecond runs after tokenFirst.
Definition sched.h:102
void(* wait)(void *pCtx, SchedToken token)
Waits until the scheduled work referenced by token finishes.
Definition sched.h:106
void(* del)(void *pCtx, SchedToken token)
Deletes any scheduler-owned resources associated with token.
Definition sched.h:110
SchedToken(* sched)(void *pCtx, const SchedTaskDesc *pDesc)
Schedules one task for execution.
Definition sched.h:78
void(* submit)(void *pCtx, SchedToken token)
Submits a previously added token.
Definition sched.h:97
SchedToken(* add)(void *pCtx, const SchedTaskDesc *pDesc)
Adds one task without submitting it for execution.
Definition sched.h:88
SchedToken(* sched_par)(void *pCtx, const SchedParDesc *pDesc)
Schedules a parallel-for workload for execution.
Definition sched.h:83