Gaia-ECS v1.0.0
A simple and powerful entity component system
Loading...
Searching...
No Matches
jobmanager.h
1#pragma once
2
3#include "gaia/config/config.h"
4#include "gaia/config/profiler.h"
5
6#include <atomic>
7#include <cinttypes>
8
9#include "gaia/cnt/ilist.h"
10#include "gaia/core/span.h"
11#include "gaia/core/utility.h"
12#include "gaia/mem/mem_alloc.h"
13#include "gaia/mt/jobcommon.h"
14#include "gaia/mt/jobhandle.h"
15
16#define GAIA_LOG_JOB_STATES 0
17
18namespace gaia {
19 namespace mt {
22 enum JobState : uint32_t {
23 DEP_BITS_START = 0,
24 DEP_BITS = 27,
25 DEP_BITS_MASK = (uint32_t)((1u << DEP_BITS) - 1),
26
27 STATE_BITS_START = DEP_BITS_START + DEP_BITS,
28 STATE_BITS = 4,
29 STATE_BITS_MASK = (uint32_t)(((1u << STATE_BITS) - 1) << STATE_BITS_START),
30
31 // STATE
32
34 Submitted = 0x01 << STATE_BITS_START,
36 Processing = 0x02 << STATE_BITS_START,
38 Executing = 0x03 << STATE_BITS_START,
40 Done = 0x04 << STATE_BITS_START,
42 Released = 0x05 << STATE_BITS_START
43 };
44
45 struct JobContainer;
46
47 namespace detail {
48 inline void signal_edge(JobContainer& jobData);
49 }
50
53 struct JobEdges {
56 union {
57 JobHandle dep;
58 JobHandle* pDeps;
59 };
61 uint32_t depCnt = 0;
62
63 JobEdges() {
64 dep = {};
65 }
66 };
67
69 struct JobContainer: cnt::ilist_item {
80 std::atomic_uint32_t state;
82 JobPriority prio;
84 JobCreationFlags flags;
86 JobEdges edges;
88 JobFunc func;
89
90 JobContainer() = default;
91 ~JobContainer() = default;
92
93 JobContainer(const JobContainer& other) = delete;
94 JobContainer& operator=(const JobContainer& other) = delete;
95
96 JobContainer(JobContainer&& other): cnt::ilist_item(GAIA_MOV(other)) {
97 state = other.state.load();
98 prio = other.prio;
99 flags = other.flags;
100 func = GAIA_MOV(other.func);
101
102 // if (edges.depCnt > 0)
103 // detail::signal_edge(*this);
104 edges = other.edges;
105 other.edges.depCnt = 0;
106 }
107 JobContainer& operator=(JobContainer&& other) {
108 GAIA_ASSERT(core::addressof(other) != this);
109 cnt::ilist_item::operator=(GAIA_MOV(other));
110 state = other.state.load();
111 prio = other.prio;
112 flags = other.flags;
113 func = GAIA_MOV(other.func);
114
115 // if (edges.depCnt > 0)
116 // detail::signal_edge(*this);
117 edges = other.edges;
118 other.edges.depCnt = 0;
119
120 return *this;
121 }
122
128 GAIA_NODISCARD static JobContainer create(uint32_t index, uint32_t generation, void* pCtx) {
129 auto* ctx = (JobAllocCtx*)pCtx;
130
131 JobContainer jc{};
132 jc.idx = index;
133 jc.data.gen = generation;
134 jc.prio = ctx->priority;
135
136 return jc;
137 }
138
142 GAIA_NODISCARD static JobHandle handle(const JobContainer& jc) {
143 return JobHandle(jc.idx, jc.data.gen, (jc.prio == JobPriority::Low) != 0);
144 }
145 };
146
148 struct ParallelCallbackHandle {
150 static constexpr uint32_t IdMask = JobHandle::IdMask;
151
152 uint32_t m_id = IdMask;
153 uint32_t m_gen = 0;
154
155 ParallelCallbackHandle() = default;
159 ParallelCallbackHandle(uint32_t id, uint32_t gen): m_id(id), m_gen(gen) {}
160
162 GAIA_NODISCARD uint32_t id() const {
163 return m_id;
164 }
165
167 GAIA_NODISCARD uint32_t gen() const {
168 return m_gen;
169 }
170
172 GAIA_NODISCARD bool operator==(const ParallelCallbackHandle& other) const {
173 return m_id == other.m_id && m_gen == other.m_gen;
174 }
175 };
176
178 struct ParallelCallbackAllocCtx {
179 JobArgsFunc callback;
180 uint32_t refs = 0;
181 };
182
184 struct ParallelCallbackRecord: cnt::ilist_item {
185 JobArgsFunc callback;
186 std::atomic_uint32_t refs = 0;
187
188 ParallelCallbackRecord() = default;
189 ~ParallelCallbackRecord() = default;
190
191 ParallelCallbackRecord(const ParallelCallbackRecord&) = delete;
192 ParallelCallbackRecord& operator=(const ParallelCallbackRecord&) = delete;
193
194 ParallelCallbackRecord(ParallelCallbackRecord&& other) noexcept:
195 cnt::ilist_item(GAIA_MOV(other)), callback(GAIA_MOV(other.callback)) {
196 refs.store(other.refs.load(std::memory_order_relaxed), std::memory_order_relaxed);
197 }
198
199 ParallelCallbackRecord& operator=(ParallelCallbackRecord&& other) noexcept {
200 GAIA_ASSERT(core::addressof(other) != this);
201 cnt::ilist_item::operator=(GAIA_MOV(other));
202 callback = GAIA_MOV(other.callback);
203 refs.store(other.refs.load(std::memory_order_relaxed), std::memory_order_relaxed);
204 return *this;
205 }
206
212 GAIA_NODISCARD static ParallelCallbackRecord create(uint32_t index, uint32_t generation, void* pCtx) {
213 auto* ctx = (ParallelCallbackAllocCtx*)pCtx;
214
215 ParallelCallbackRecord record{};
216 record.idx = index;
217 record.data.gen = generation;
218 record.callback = GAIA_MOV(ctx->callback);
219 record.refs.store(ctx->refs, std::memory_order_relaxed);
220 return record;
221 }
222
226 GAIA_NODISCARD static ParallelCallbackHandle handle(const ParallelCallbackRecord& record) {
227 return ParallelCallbackHandle(record.idx, record.data.gen);
228 }
229 };
230
232 class JobManager {
233 using JobDataLayout = cnt::paged_ilist<JobContainer, JobHandle>;
234 static constexpr uint32_t JobDataPageCount = JobDataLayout::page_count_for_capacity(JobHandle::IdMask);
235 using ParallelCallbackLayout = cnt::paged_ilist<ParallelCallbackRecord, ParallelCallbackHandle>;
236 static constexpr uint32_t ParallelCallbackPageCount =
237 ParallelCallbackLayout::page_count_for_capacity(ParallelCallbackHandle::IdMask);
238
241 cnt::paged_ilist<JobContainer, JobHandle, JobDataPageCount> m_jobData;
244 cnt::paged_ilist<ParallelCallbackRecord, ParallelCallbackHandle, ParallelCallbackPageCount> m_parallelCallbacks;
245
246 public:
250 GAIA_NODISCARD bool valid(JobHandle jobHandle) const {
251 return m_jobData.has(jobHandle);
252 }
253
256 JobContainer& data(JobHandle jobHandle) {
257 return m_jobData.payload_unsafe(jobHandle.id());
258 }
261 const JobContainer& data(JobHandle jobHandle) const {
262 return m_jobData.payload_unsafe(jobHandle.id());
263 }
264
268 GAIA_NODISCARD JobHandle alloc_job(Job job) {
269 JobAllocCtx ctx{job.priority};
270
271 auto handle = m_jobData.alloc(&ctx);
272 auto& j = m_jobData[handle.id()];
273
274 // Make sure there is not state yet
275 GAIA_ASSERT(j.state == 0 || j.state == JobState::Released);
276
277 j.edges = {};
278 j.prio = ctx.priority;
279 j.state.store(0);
280 j.func = GAIA_MOV(job.func);
281 j.flags = job.flags;
282 return handle;
283 }
284
289 GAIA_NODISCARD ParallelCallbackHandle alloc_parallel_callback(JobArgsFunc callback, uint32_t refs) {
290 ParallelCallbackAllocCtx ctx{};
291 ctx.callback = GAIA_MOV(callback);
292 ctx.refs = refs;
293 return m_parallelCallbacks.alloc(&ctx);
294 }
295
302 GAIA_NODISCARD JobFunc free_job(JobHandle jobHandle) {
303 auto& jobData = m_jobData.payload_unsafe(jobHandle.id());
304 GAIA_ASSERT(is_clear(jobData) || done(jobData));
305 JobFunc func = GAIA_MOV(jobData.func);
306 free_edges(jobData);
307 jobData.state.store(JobState::Released, std::memory_order_release);
308 jobData.data.gen = (jobHandle.gen() + 1) & JobHandle::GenMask;
309 m_jobData.free_keep_live(jobHandle);
310 return func;
311 }
312
317 GAIA_NODISCARD JobArgsFunc free_parallel_callback(ParallelCallbackHandle handle) {
318 auto& record = m_parallelCallbacks[handle.id()];
319 JobArgsFunc callback = GAIA_MOV(record.callback);
320 record.refs.store(0, std::memory_order_relaxed);
321 m_parallelCallbacks.free(handle);
322 return callback;
323 }
324
326 void reset() {
327 m_jobData.clear();
328 m_parallelCallbacks.clear();
329 }
330
333 static void run(JobContainer& jobData) {
334 if (jobData.func.operator bool())
335 jobData.func();
336 }
337
341 static bool signal_edge(JobContainer& jobData) {
342 // Subtract from dependency counter
343 const auto state = jobData.state.fetch_sub(1, std::memory_order_acq_rel) - 1;
344
345 // If the job is not submitted, we can't accept it
346 const auto s = state & JobState::STATE_BITS_MASK;
347 if (s != JobState::Submitted)
348 return false;
349
350 // If the job still has some dependencies left we can't accept it
351 const auto deps = state & JobState::DEP_BITS_MASK;
352 return deps == 0;
353 }
354
357 static void free_edges(JobContainer& jobData) {
358 // We only allocate an array for 2 and more dependencies
359 if (jobData.edges.depCnt <= 1) {
360 jobData.edges = {};
361 return;
362 }
363
364 mem::AllocHelper::free(jobData.edges.pDeps);
365 jobData.edges = {};
366 }
367
373 void dep(JobHandle jobFirst, JobHandle jobSecond) {
374 dep(std::span(&jobFirst, 1), jobSecond);
375 }
376
382 void dep(std::span<JobHandle> jobsFirst, JobHandle jobSecond) {
383 GAIA_ASSERT(!jobsFirst.empty());
384
385 GAIA_PROF_SCOPE(JobManager::dep);
386
387 auto& secondData = data(jobSecond);
388
389#if GAIA_ASSERT_ENABLED
390 GAIA_ASSERT(!busy(const_cast<const JobContainer&>(secondData)));
391 for (auto jobFirst: jobsFirst) {
392 const auto& firstData = data(jobFirst);
393 GAIA_ASSERT(!busy(firstData));
394 }
395#endif
396
397 for (auto jobFirst: jobsFirst)
398 dep_internal(jobFirst, jobSecond);
399
400 // Tell jobSecond that it has new dependencies
401 // secondData.canWait = true;
402 const uint32_t cnt = (uint32_t)jobsFirst.size();
403 [[maybe_unused]] const uint32_t statePrev = secondData.state.fetch_add(cnt);
404 GAIA_ASSERT((statePrev & JobState::DEP_BITS_MASK) < DEP_BITS_MASK - 1);
405 }
406
413 void dep_refresh(std::span<JobHandle> jobsFirst, JobHandle jobSecond) {
414 GAIA_ASSERT(!jobsFirst.empty());
415
416 GAIA_PROF_SCOPE(JobManager::dep_refresh);
417
418 auto& secondData = data(jobSecond);
419
420#if GAIA_ASSERT_ENABLED
421 GAIA_ASSERT(!busy(const_cast<const JobContainer&>(secondData)));
422 for (auto jobFirst: jobsFirst) {
423 const auto& firstData = data(jobFirst);
424 GAIA_ASSERT(!busy(firstData));
425 }
426
427 for (auto jobFirst: jobsFirst)
428 dep_refresh_internal(jobFirst, jobSecond);
429#endif
430
431 // Tell jobSecond that it has new dependencies
432 // secondData.canWait = true;
433 const uint32_t cnt = (uint32_t)jobsFirst.size();
434 [[maybe_unused]] const uint32_t statePrev = secondData.state.fetch_add(cnt);
435 GAIA_ASSERT((statePrev & JobState::DEP_BITS_MASK) < DEP_BITS_MASK - 1);
436 }
437
441 static uint32_t submit(JobContainer& jobData) {
442 [[maybe_unused]] const auto state = jobData.state.load(std::memory_order_acquire) & JobState::STATE_BITS_MASK;
443 GAIA_ASSERT(state < JobState::Submitted);
444 const auto val =
445 jobData.state.fetch_add(JobState::Submitted, std::memory_order_release) + (uint32_t)JobState::Submitted;
446#if GAIA_LOG_JOB_STATES
447 GAIA_LOG_N("JobHandle %u.%u - SUBMITTED", jobData.idx, jobData.gen);
448#endif
449 return val;
450 }
451
454 static void processing(JobContainer& jobData) {
455 GAIA_ASSERT(submitted(const_cast<const JobContainer&>(jobData)));
456 jobData.state.store(JobState::Processing, std::memory_order_release);
457#if GAIA_LOG_JOB_STATES
458 GAIA_LOG_N("JobHandle %u.%u - PROCESSING", jobData.idx, jobData.gen);
459#endif
460 }
461
470 static void executing(JobContainer& jobData, uint32_t workerIdx) {
471 GAIA_ASSERT(processing(const_cast<const JobContainer&>(jobData)));
472 jobData.state.store(JobState::Executing | workerIdx, std::memory_order_seq_cst);
473#if GAIA_LOG_JOB_STATES
474 GAIA_LOG_N("JobHandle %u.%u - EXECUTING", jobData.idx, jobData.gen);
475#endif
476 }
477
480 static void finalize(JobContainer& jobData) {
481 jobData.state.store(JobState::Done, std::memory_order_release);
482#if GAIA_LOG_JOB_STATES
483 GAIA_LOG_N("JobHandle %u.%u - DONE", jobData.idx, jobData.gen);
484#endif
485 }
486
489 static void reset_state(JobContainer& jobData) {
490 [[maybe_unused]] const auto state = jobData.state.load() & JobState::STATE_BITS_MASK;
491 // The job needs to be either clear or finalize for us to allow a reset
492 GAIA_ASSERT(state == 0 || state == JobState::Done);
493 jobData.state.store(0);
494#if GAIA_LOG_JOB_STATES
495 GAIA_LOG_N("JobHandle %u.%u - RESET_STATE", jobData.idx, jobData.gen);
496#endif
497 }
498
502 GAIA_NODISCARD bool is_clear(JobHandle jobHandle) const {
503 const auto& jobData = data(jobHandle);
504 const auto state = jobData.state.load();
505 return state == 0;
506 }
507
511 GAIA_NODISCARD static bool is_clear(JobContainer& jobData) {
512 const auto state = jobData.state.load();
513 return state == 0;
514 }
515
519 GAIA_NODISCARD static bool submitted(const JobContainer& jobData) {
520 const auto state = jobData.state.load(std::memory_order_acquire) & JobState::STATE_BITS_MASK;
521 return state == JobState::Submitted;
522 }
523
527 GAIA_NODISCARD static bool processing(const JobContainer& jobData) {
528 const auto state = jobData.state.load(std::memory_order_acquire) & JobState::STATE_BITS_MASK;
529 return state == JobState::Processing;
530 }
531
535 GAIA_NODISCARD static bool busy(const JobContainer& jobData) {
536 const auto state = jobData.state.load() & JobState::STATE_BITS_MASK;
537 return state == JobState::Executing || state == JobState::Processing;
538 }
539
543 GAIA_NODISCARD static bool done(const JobContainer& jobData) {
544 const auto state = jobData.state.load(std::memory_order_acquire) & JobState::STATE_BITS_MASK;
545 return state == JobState::Done;
546 }
547
551 void invoke_parallel_callback(ParallelCallbackHandle handle, const JobArgs& args) {
552 auto& record = m_parallelCallbacks[handle.id()];
553 GAIA_ASSERT(record.data.gen == handle.gen());
554 record.callback(args);
555 }
556
560 GAIA_NODISCARD bool release_parallel_callback_ref(ParallelCallbackHandle handle) {
561 auto& record = m_parallelCallbacks[handle.id()];
562 GAIA_ASSERT(record.data.gen == handle.gen());
563 return record.refs.fetch_sub(1, std::memory_order_acq_rel) == 1;
564 }
565
566 private:
567 void dep_internal(JobHandle jobFirst, JobHandle jobSecond) {
568 GAIA_ASSERT(jobFirst != (JobHandle)JobNull_t{});
569 GAIA_ASSERT(jobSecond != (JobHandle)JobNull_t{});
570
571 auto& firstData = data(jobFirst);
572 const auto depCnt0 = firstData.edges.depCnt;
573 const auto depCnt1 = ++firstData.edges.depCnt;
574
575#if GAIA_LOG_JOB_STATES
576 GAIA_LOG_N(
577 "DEP %u.%u, %u -> %u.%u", jobFirst.id(), jobFirst.gen(), firstData.edges.depCnt, jobSecond.id(),
578 jobSecond.gen());
579#endif
580
581 if (depCnt1 <= 1) {
582 firstData.edges.dep = jobSecond;
583 } else if (depCnt1 == 2) {
584 auto prev = firstData.edges.dep;
585 // TODO: Use custom allocator
586 firstData.edges.pDeps = mem::AllocHelper::alloc<JobHandle>(depCnt1);
587 firstData.edges.pDeps[0] = prev;
588 firstData.edges.pDeps[1] = jobSecond;
589 } else {
590 // Reallocate if the previous value was a power of 2
591 const bool isPow2 = core::is_pow2(depCnt0);
592 if (isPow2) {
593 const auto nextPow2 = depCnt0 << 1;
594 auto* pPrev = firstData.edges.pDeps;
595 // TODO: Use custom allocator
596 firstData.edges.pDeps = mem::AllocHelper::alloc<JobHandle>(nextPow2);
597 if (pPrev != nullptr) {
598 GAIA_FOR(depCnt0) firstData.edges.pDeps[i] = pPrev[i];
599 mem::AllocHelper::free(pPrev);
600 }
601 }
602
603 // Append new dependencies
604 firstData.edges.pDeps[depCnt0] = jobSecond;
605 }
606 }
607
608#if GAIA_ASSERT_ENABLED
609 void dep_refresh_internal(JobHandle jobFirst, JobHandle jobSecond) const {
610 GAIA_ASSERT(jobFirst != (JobHandle)JobNull_t{});
611 GAIA_ASSERT(jobSecond != (JobHandle)JobNull_t{});
612
613 const auto& firstData = data(jobFirst);
614 const auto depCnt = firstData.edges.depCnt;
615
616 if (depCnt <= 1) {
617 GAIA_ASSERT(firstData.edges.dep == jobSecond);
618 } else {
619 GAIA_ASSERT(firstData.edges.pDeps != nullptr);
620 bool found = false;
621 GAIA_FOR(firstData.edges.depCnt) {
622 if (firstData.edges.pDeps[i] == jobSecond) {
623 found = true;
624 break;
625 }
626 }
627 GAIA_ASSERT(found);
628 }
629 }
630#endif
631 };
632
633 namespace detail {
634 void signal_edge(JobContainer& jobData) {
635 JobManager::signal_edge(jobData);
636 }
637 } // namespace detail
639 } // namespace mt
640} // namespace gaia