Gaia-ECS v1.0.0
A simple and powerful entity component system
Loading...
Searching...
No Matches
threadpool.h
1#pragma once
2
3#include "gaia/config/config.h"
4#include "gaia/config/profiler.h"
5
6#if GAIA_PLATFORM_WINDOWS
7 #include <cstdio>
8 #include <cstdlib>
9
10 #include "gaia/mt/win32.h"
11#endif
12
13#define GAIA_THREAD_OFF 0
14#define GAIA_THREAD_STD 1
15#define GAIA_THREAD_PTHREAD 2
16
17#if GAIA_PLATFORM_WINDOWS || GAIA_PLATFORM_WASM
18 #include <thread>
19 // Emscripten supports std::thread if compiled with -sUSE_PTHREADS=1.
20 // Otherwise, std::thread calls are no-ops that compile but do not run concurrently.
21 #define GAIA_THREAD std::thread
22 #define GAIA_THREAD_PLATFORM GAIA_THREAD_STD
23#elif GAIA_PLATFORM_APPLE
24 #include <pthread.h>
25 #include <pthread/sched.h>
26 #include <sys/qos.h>
27 #include <sys/sysctl.h>
28 #define GAIA_THREAD pthread_t
29 #define GAIA_THREAD_PLATFORM GAIA_THREAD_PTHREAD
30#elif GAIA_PLATFORM_LINUX
31 #include <dirent.h>
32 #include <fcntl.h>
33 #include <pthread.h>
34 #include <unistd.h>
35 #define GAIA_THREAD pthread_t
36 #define GAIA_THREAD_PLATFORM GAIA_THREAD_PTHREAD
37#elif GAIA_PLATFORM_FREEBSD
38 #include <pthread.h>
39 #include <sys/sysctl.h>
40 #define GAIA_THREAD pthread_t
41 #define GAIA_THREAD_PLATFORM GAIA_THREAD_PTHREAD
42#endif
43
44#if GAIA_PLATFORM_WINDOWS
45 #include <malloc.h>
46#else
47 #include <alloca.h>
48#endif
49#include <atomic>
50#include <thread>
51
52#include "gaia/cnt/sarray_ext.h"
53#include "gaia/core/span.h"
54#include "gaia/core/utility.h"
55#include "gaia/util/logging.h"
56
57#include "gaia/mt/event.h"
58#include "gaia/mt/futex.h"
59#include "gaia/mt/jobcommon.h"
60#include "gaia/mt/jobhandle.h"
61#include "gaia/mt/jobmanager.h"
62#include "gaia/mt/jobqueue.h"
63#include "gaia/mt/semaphore_fast.h"
64#include "gaia/mt/spinlock.h"
65
66namespace gaia {
67 namespace mt {
68
69 namespace detail {
71 inline thread_local ThreadCtx* tl_workerCtx;
72 } // namespace detail
73
74 GAIA_MSVC_WARNING_PUSH()
75 GAIA_MSVC_WARNING_DISABLE(4324)
76
78 class GAIA_API ThreadPool final {
79 friend class JobManager;
80
85 static constexpr uint32_t MaxWorkers = JobState::DEP_BITS;
86
88 std::thread::id m_mainThreadId;
89
91 std::atomic_bool m_stop{};
95 alignas(128) cnt::sarray_ext<ThreadCtx, MaxWorkers> m_workersCtx;
97 MpmcQueue<JobHandle, 1024> m_jobQueue[JobPriorityCnt];
99 MpmcQueue<JobHandle, 1024> m_jobQueueBackground;
101 uint32_t m_frameWorkersCnt = 0;
103 uint32_t m_backgroundWorkersCnt = 0;
105 uint32_t m_workersCnt[JobPriorityCnt]{};
110 uint32_t m_workerThreadsCnt[JobPriorityCnt]{};
112 SemaphoreFast m_sem[JobPriorityCnt];
114 SemaphoreFast m_semBackground;
115
117 std::atomic_uint32_t m_blockedInWorkUntil;
118
120 JobManager m_jobManager;
126 GAIA_PROF_MUTEX(SpinLock, m_jobAllocMtx);
127
128#if GAIA_ECS_TEST_HOOKS
129 public:
132 inline static void (*s_testBeforeFinalize)(JobHandle jobHandle) = nullptr;
133#endif
134
135 private:
138 static void test_before_finalize([[maybe_unused]] JobHandle jobHandle) {
139#if GAIA_ECS_TEST_HOOKS
140 if (s_testBeforeFinalize != nullptr)
141 s_testBeforeFinalize(jobHandle);
142#endif
143 }
144
145 private:
146 ThreadPool() {
147 m_stop.store(false);
148
149 make_main_thread();
150
151 const auto hwThreads = hw_thread_cnt();
152 const auto hwEffThreads = hw_efficiency_cores_cnt();
153 uint32_t hiPrioWorkers = hwThreads;
154 if (hwEffThreads < hwThreads)
155 hiPrioWorkers -= hwEffThreads;
156
157 set_max_workers(hwThreads, hiPrioWorkers);
158 }
159
160 ThreadPool(ThreadPool&&) = delete;
161 ThreadPool(const ThreadPool&) = delete;
162 ThreadPool& operator=(ThreadPool&&) = delete;
163 ThreadPool& operator=(const ThreadPool&) = delete;
164
165 public:
168 static ThreadPool& get() {
169 static ThreadPool threadPool;
170 return threadPool;
171 }
172
173 ~ThreadPool() {
174 reset();
175 }
176
179 m_mainThreadId = std::this_thread::get_id();
180 if (!m_workersCtx.empty())
181 detail::tl_workerCtx = &m_workersCtx[0];
182 }
183
186 GAIA_NODISCARD uint32_t workers() const {
187 return m_frameWorkersCnt;
188 }
189
192 GAIA_NODISCARD uint32_t background_workers() const {
193 return m_backgroundWorkersCnt;
194 }
195
202 void set_max_workers(uint32_t count, uint32_t countHighPrio) {
203 const auto maxFrameWorkers = MaxWorkers - m_backgroundWorkersCnt;
204 const auto workersCnt = core::get_max(core::get_min(maxFrameWorkers, count), 1U);
205 countHighPrio = core::get_min(countHighPrio, workersCnt);
206
207 // Stop all threads first
208 reset();
209
210 // Reset previous worker contexts
211 for (auto& ctx: m_workersCtx)
212 ctx.reset();
213
214 m_frameWorkersCnt = workersCnt - 1;
215
216 // The main thread uses context 0. Frame workers follow, and
217 // background workers are appended after them.
218 m_workersCtx.resize(workersCnt + m_backgroundWorkersCnt);
219 // We also have the main thread so there's always one less worker spawned
220 m_workers.resize(m_frameWorkersCnt + m_backgroundWorkersCnt);
221
222 // First worker is considered the main thread.
223 // It is also assigned high priority but it doesn't really matter.
224 // The main thread can steal any jobs, both low and high priority.
225 detail::tl_workerCtx = m_workersCtx.data();
226 m_workersCtx[0].tp = this;
227 m_workersCtx[0].workerIdx = 0;
228 m_workersCtx[0].prio = JobPriority::High;
229
230 // Reset the workers
231 for (auto& worker: m_workers)
232 worker = {};
233
234 // Create a new set of high and low priority threads (if any)
235 uint32_t workerIdx = 1;
236 set_workers_high_prio_inter(workerIdx, countHighPrio);
237 create_background_worker_threads(workerIdx);
238 }
239
244 void set_workers_high_prio_inter(uint32_t& workerIdx, uint32_t count) {
245 count = gaia::core::get_min(count, m_frameWorkersCnt);
246 m_workerThreadsCnt[0] = count;
247 m_workerThreadsCnt[1] = m_frameWorkersCnt - count;
248 m_workersCnt[0] = count + 1; // Main thread is always a priority worker
249 m_workersCnt[1] = m_workerThreadsCnt[1];
250
251 // Create a new set of high and low priority threads (if any)
252 create_worker_threads(workerIdx, JobPriority::High, m_workerThreadsCnt[0]);
253 create_worker_threads(workerIdx, JobPriority::Low, m_workerThreadsCnt[1]);
254 }
255
260 void set_workers_low_prio_inter(uint32_t& workerIdx, uint32_t count) {
261 const uint32_t realCnt = gaia::core::get_min(count, m_frameWorkersCnt);
262 m_workerThreadsCnt[0] = m_frameWorkersCnt - realCnt;
263 m_workerThreadsCnt[1] = realCnt;
264 m_workersCnt[0] = m_workerThreadsCnt[0] + 1; // Main thread is always a priority worker
265 m_workersCnt[1] = m_workerThreadsCnt[1];
266
267 // Create a new set of high and low priority threads (if any)
268 create_worker_threads(workerIdx, JobPriority::High, m_workerThreadsCnt[0]);
269 create_worker_threads(workerIdx, JobPriority::Low, m_workerThreadsCnt[1]);
270 }
271
274 void set_workers_high_prio(uint32_t count) {
275 // Stop all threads first
276 reset();
277 detail::tl_workerCtx = m_workersCtx.data();
278
279 uint32_t workerIdx = 1;
280 set_workers_high_prio_inter(workerIdx, count);
281 create_background_worker_threads(workerIdx);
282 }
283
286 void set_workers_low_prio(uint32_t count) {
287 // Stop all threads first
288 reset();
289 detail::tl_workerCtx = m_workersCtx.data();
290
291 uint32_t workerIdx = 1;
292 set_workers_low_prio_inter(workerIdx, count);
293 create_background_worker_threads(workerIdx);
294 }
295
301 void set_background_workers(uint32_t count) {
302 const auto maxBackgroundWorkers = MaxWorkers - 1;
303 count = core::get_min(maxBackgroundWorkers, count);
304
305 const auto frameWorkersCntOld = m_frameWorkersCnt;
306 const auto highWorkersCntOld = m_workerThreadsCnt[0];
307
308 // Stop all threads first
309 reset();
310
311 m_backgroundWorkersCnt = count;
312
313 const auto maxFrameWorkers = MaxWorkers - m_backgroundWorkersCnt - 1;
314 m_frameWorkersCnt = core::get_min(frameWorkersCntOld, maxFrameWorkers);
315
316 for (auto& ctx: m_workersCtx)
317 ctx.reset();
318
319 m_workersCtx.resize(m_frameWorkersCnt + 1 + m_backgroundWorkersCnt);
320 m_workers.resize(m_frameWorkersCnt + m_backgroundWorkersCnt);
321
322 detail::tl_workerCtx = m_workersCtx.data();
323 m_workersCtx[0].tp = this;
324 m_workersCtx[0].workerIdx = 0;
325 m_workersCtx[0].prio = JobPriority::High;
326
327 for (auto& worker: m_workers)
328 worker = {};
329
330 uint32_t workerIdx = 1;
331 set_workers_high_prio_inter(workerIdx, highWorkersCntOld);
332 create_background_worker_threads(workerIdx);
333 }
334
340 void dep(JobHandle jobFirst, JobHandle jobSecond) {
341 GAIA_ASSERT(main_thread());
342
343 m_jobManager.dep(std::span(&jobFirst, 1), jobSecond);
344 }
345
351 void dep(std::span<JobHandle> jobsFirst, JobHandle jobSecond) {
352 GAIA_ASSERT(main_thread());
353
354 m_jobManager.dep(jobsFirst, jobSecond);
355 }
356
364 void dep_refresh(JobHandle jobFirst, JobHandle jobSecond) {
365 GAIA_ASSERT(main_thread());
366
367 m_jobManager.dep_refresh(std::span(&jobFirst, 1), jobSecond);
368 }
369
377 void dep_refresh(std::span<JobHandle> jobsFirst, JobHandle jobSecond) {
378 GAIA_ASSERT(main_thread());
379
380 m_jobManager.dep_refresh(jobsFirst, jobSecond);
381 }
382
390 template <typename TJob>
391 JobHandle add(TJob job) {
392 GAIA_ASSERT(main_thread());
393
394 job.priority = final_prio(job);
395
396 auto& mtx = GAIA_PROF_EXTRACT_MUTEX(m_jobAllocMtx);
397 core::lock_scope lock(mtx);
398 GAIA_PROF_LOCK_MARK(m_jobAllocMtx);
399
400 return m_jobManager.alloc_job(GAIA_MOV(job));
401 }
402
403 private:
404 void add_n(JobPriority prio, std::span<JobHandle> jobHandles) {
405 GAIA_ASSERT(main_thread());
406 GAIA_ASSERT(!jobHandles.empty());
407
408 auto& mtx = GAIA_PROF_EXTRACT_MUTEX(m_jobAllocMtx);
409 core::lock_scope lock(mtx);
410 GAIA_PROF_LOCK_MARK(m_jobAllocMtx);
411
412 for (auto& jobHandle: jobHandles)
413 jobHandle = m_jobManager.alloc_job({{}, prio, JobCreationFlags::Default});
414 }
415
416 GAIA_NODISCARD ParallelCallbackHandle add_parallel_callback(JobArgsFunc callback, uint32_t refs) {
417 auto& mtx = GAIA_PROF_EXTRACT_MUTEX(m_jobAllocMtx);
418 core::lock_scope lock(mtx);
419 GAIA_PROF_LOCK_MARK(m_jobAllocMtx);
420
421 return m_jobManager.alloc_parallel_callback(GAIA_MOV(callback), refs);
422 }
423
424 void release_parallel_callback(ParallelCallbackHandle handle) {
425 if (!m_jobManager.release_parallel_callback_ref(handle))
426 return;
427
428 // Destroyed once the lock is released so the callback's destructor may use the thread pool.
429 JobArgsFunc callback;
430 {
431 auto& mtx = GAIA_PROF_EXTRACT_MUTEX(m_jobAllocMtx);
432 core::lock_scope lock(mtx);
433 GAIA_PROF_LOCK_MARK(m_jobAllocMtx);
434
435 callback = m_jobManager.free_parallel_callback(handle);
436 }
437 }
438
439 void release_job(JobHandle jobHandle) {
440 // run() already ended the callable's lifetime. What is left, a trivially destructible callable or heap
441 // storage, is released once the lock is released.
442 JobFunc func;
443 {
444 auto& mtx = GAIA_PROF_EXTRACT_MUTEX(m_jobAllocMtx);
445 core::lock_scope lock(mtx);
446 GAIA_PROF_LOCK_MARK(m_jobAllocMtx);
447
448 func = m_jobManager.free_job(jobHandle);
449 }
450 }
451
458 GAIA_NODISCARD JobFunc
459 parallel_group_func(ParallelCallbackHandle callbackHandle, uint32_t idxStart, uint32_t idxEnd) {
460 auto func = [this, callbackHandle, idxStart, idxEnd]() {
461 JobArgs args;
462 args.idxStart = idxStart;
463 args.idxEnd = idxEnd;
464 m_jobManager.invoke_parallel_callback(callbackHandle, args);
465 release_parallel_callback(callbackHandle);
466 };
467 static_assert(JobFunc::stores_inline<decltype(func)>(), "Parallel group jobs must not allocate");
468 return JobFunc(GAIA_MOV(func));
469 }
470
476 GAIA_NODISCARD static JobFunc parallel_group_func(const JobParallelRef& job, uint32_t idxStart, uint32_t idxEnd) {
477 auto func = [pCtx = job.pCtx, invoke = job.invoke, idxStart, idxEnd]() {
478 JobArgs args;
479 args.idxStart = idxStart;
480 args.idxEnd = idxEnd;
481 invoke(pCtx, args);
482 };
483 static_assert(JobFunc::stores_inline<decltype(func)>(), "Parallel group jobs must not allocate");
484 return JobFunc(GAIA_MOV(func));
485 }
486
487 public:
493 void del([[maybe_unused]] JobHandle jobHandle) {
494 GAIA_ASSERT(jobHandle != (JobHandle)JobNull_t{});
495
496 // Destroyed once the lock is released so the callable's destructor may use the thread pool.
497 JobFunc func;
498 {
499 auto& mtx = GAIA_PROF_EXTRACT_MUTEX(m_jobAllocMtx);
500 core::lock_scope lock(mtx);
501 GAIA_PROF_LOCK_MARK(m_jobAllocMtx);
502 if (!m_jobManager.valid(jobHandle))
503 return;
504
505#if GAIA_ASSERT_ENABLED
506 {
507 const auto& jobData = m_jobManager.data(jobHandle);
508 GAIA_ASSERT(jobData.state == 0 || m_jobManager.done(jobData));
509 }
510#endif
511
512 func = m_jobManager.free_job(jobHandle);
513 }
514 }
515
522 void submit(std::span<JobHandle> jobHandles) {
523 if (jobHandles.empty())
524 return;
525
526 GAIA_PROF_SCOPE(tp::submitn);
527
528 auto* pHandles = (JobHandle*)alloca(sizeof(JobHandle) * jobHandles.size());
529
530 uint32_t cnt = 0;
531 for (auto handle: jobHandles) {
532 GAIA_ASSERT(handle != (JobHandle)JobNull_t{});
533
534 auto& jobData = m_jobManager.data(handle);
535 if GAIA_UNLIKELY (jobData.data.gen != handle.gen())
536 continue;
537
538 const auto state = m_jobManager.submit(jobData) & JobState::DEP_BITS_MASK;
539 // Jobs that were already submitted won't be submitted again.
540 // We can only accept the job if it has no pending dependencies.
541 if (state != 0)
542 continue;
543
544 pHandles[cnt++] = handle;
545 }
546
547 auto* ctx = detail::tl_workerCtx;
548 process(std::span(pHandles, cnt), ctx);
549 }
550
557 void submit(JobHandle jobHandle) {
558 GAIA_ASSERT(jobHandle != (JobHandle)JobNull_t{});
559 GAIA_PROF_SCOPE(tp::submit);
560
561 auto& jobData = m_jobManager.data(jobHandle);
562 if GAIA_UNLIKELY (jobData.data.gen != jobHandle.gen())
563 return;
564
565 const auto state = m_jobManager.submit(jobData) & JobState::DEP_BITS_MASK;
566 if (state != 0)
567 return;
568
569 auto* ctx = detail::tl_workerCtx;
570 process(std::span(&jobHandle, 1), ctx);
571 }
572
575 void reset_state(std::span<JobHandle> jobHandles) {
576 if (jobHandles.empty())
577 return;
578
579 GAIA_PROF_SCOPE(tp::reset);
580
581 for (auto handle: jobHandles) {
582 auto& jobData = m_jobManager.data(handle);
583 m_jobManager.reset_state(jobData);
584 }
585 }
586
589 void reset_state(JobHandle jobHandle) {
590 reset_state(std::span(&jobHandle, 1));
591 }
592
596 void reset(std::span<JobHandle> jobHandles) {
597 if (jobHandles.empty())
598 return;
599
600 GAIA_ASSERT(main_thread());
601 GAIA_PROF_SCOPE(tp::reset_wait);
602
603 // Wait first to avoid resetting one handle while another one still depends on it.
604 for (auto handle: jobHandles) {
605 if (handle == (JobHandle)JobNull_t{})
606 continue;
607 wait(handle);
608 }
609
610 for (auto handle: jobHandles) {
611 if (handle == (JobHandle)JobNull_t{})
612 continue;
613
614 auto& jobData = m_jobManager.data(handle);
615 const auto state = jobData.state.load() & JobState::STATE_BITS_MASK;
616 // Auto-deleted jobs are released and cannot be reused through reset_state().
617 if (state == JobState::Released)
618 continue;
619
620 m_jobManager.reset_state(jobData);
621 }
622 }
623
626 void reset(JobHandle jobHandle) {
627 reset(std::span(&jobHandle, 1));
628 }
629
636 JobHandle jobHandle = add(GAIA_MOV(job));
637 submit(jobHandle);
638 return jobHandle;
639 }
640
650 job.flags = (JobCreationFlags)((uint8_t)job.flags | (uint8_t)JobCreationFlags::Background);
651 JobHandle jobHandle = add(GAIA_MOV(job));
652 submit(jobHandle);
653 return jobHandle;
654 }
655
662 JobHandle sched(Job job, JobHandle dependsOn) {
663 JobHandle jobHandle = add(GAIA_MOV(job));
664 dep(dependsOn, jobHandle);
665 submit(jobHandle);
666 return jobHandle;
667 }
668
676 JobHandle sched_par(JobParallel job, uint32_t itemsToProcess, uint32_t groupSize) {
677 GAIA_ASSERT(main_thread());
678
679 // Empty data set are considered wrong inputs
680 GAIA_ASSERT(itemsToProcess != 0);
681 if (itemsToProcess == 0)
682 return JobNull;
683
684 // Don't add new jobs once stop was requested
685 if GAIA_UNLIKELY (m_stop)
686 return JobNull;
687
688 // Make sure the right priority is selected
689 const auto prio = job.priority = final_prio(job);
690
691 // No group size was given, make a guess based on the set size
692 if (groupSize == 0) {
693 const auto cntWorkers = core::get_max(1U, m_workersCnt[(uint32_t)prio]);
694 groupSize = itemsToProcess / cntWorkers + (itemsToProcess % cntWorkers != 0);
695
696 // If there are too many items we split them into multiple jobs.
697 // This way, if we wait for the result and some workers finish
698 // with our task faster, the finished worker can pick up a new
699 // job faster.
700 // On the other hand, too little items probably don't deserve
701 // multiple jobs.
702 constexpr uint32_t maxUnitsOfWorkPerGroup = 8;
703 groupSize = groupSize / maxUnitsOfWorkPerGroup;
704 if (groupSize <= 0)
705 groupSize = 1;
706 }
707
708 const auto jobs = itemsToProcess / groupSize + (itemsToProcess % groupSize != 0);
709
710 // Only one job is created, use the job directly.
711 // Generally, this is the case we would want to avoid because it means this particular case
712 // is not worth of being scheduled via sched_par. However, we can never know for sure what
713 // the reason for that is so let's stay silent.
714 if (jobs == 1) {
715 const uint32_t groupJobIdxEnd = groupSize < itemsToProcess ? groupSize : itemsToProcess;
716 // The only job owns the callback directly. The closure is larger than JobFunc::InlineSize, so it is
717 // allocated on the general heap and destroyed by the worker once the job ran. That is cheaper than a
718 // shared callback record, which would take the job allocation lock twice.
719 auto groupJobFunc = [func = GAIA_MOV(job.func), groupJobIdxEnd]() {
720 JobArgs args;
721 args.idxStart = 0;
722 args.idxEnd = groupJobIdxEnd;
723 func(args);
724 };
725 auto handle = add(Job{GAIA_MOV(groupJobFunc), prio, JobCreationFlags::Default});
726 submit(handle);
727 return handle;
728 }
729
730 // Multiple jobs need to be parallelized. They share the callback through the callback pool. The last job
731 // to finish with it destroys it, before that job is reported done. Each job stores only an inline callable.
732 auto callbackHandle = add_parallel_callback(GAIA_MOV(job.func), jobs);
733
734 // Create a sync job and assign it as their dependency.
735
736 auto* pHandles = (JobHandle*)alloca(sizeof(JobHandle) * (jobs + 1));
737 std::span<JobHandle> handles(pHandles, jobs + 1);
738
739 add_n(prio, handles);
740
741#if GAIA_ASSERT_ENABLED
742 for (auto jobHandle: handles)
743 GAIA_ASSERT(m_jobManager.is_clear(jobHandle));
744#endif
745
746 // Work jobs
747 for (uint32_t jobIndex = 0; jobIndex < jobs; ++jobIndex) {
748 const uint32_t groupJobIdxStart = jobIndex * groupSize;
749 const uint32_t groupJobIdxEnd =
750 core::get_min(groupSize, itemsToProcess - groupJobIdxStart) + groupJobIdxStart;
751
752 auto& jobData = m_jobManager.data(pHandles[jobIndex]);
753 jobData.func = parallel_group_func(callbackHandle, groupJobIdxStart, groupJobIdxEnd);
754 jobData.prio = prio;
755 }
756 // Sync job
757 {
758 auto& jobData = m_jobManager.data(pHandles[jobs]);
759 jobData.prio = prio;
760 }
761
762 // Assign the sync jobs as a dependency for work jobs
763 dep(handles.subspan(0, jobs), pHandles[jobs]);
764
765 // Sumbit the jobs to the threadpool.
766 // This is a point of no return. After this point no more changes to jobs are possible.
767 submit(handles);
768 return pHandles[jobs];
769 }
770
778 JobHandle sched_par(JobParallelRef job, uint32_t itemsToProcess, uint32_t groupSize) {
779 GAIA_ASSERT(main_thread());
780 GAIA_ASSERT(job.pCtx != nullptr);
781 GAIA_ASSERT(job.invoke != nullptr);
782
783 GAIA_ASSERT(itemsToProcess != 0);
784 if (itemsToProcess == 0)
785 return JobNull;
786
787 if GAIA_UNLIKELY (m_stop)
788 return JobNull;
789
790 const auto prio = job.priority = final_prio(job);
791
792 if (groupSize == 0) {
793 const auto cntWorkers = core::get_max(1U, m_workersCnt[(uint32_t)prio]);
794 groupSize = itemsToProcess / cntWorkers + (itemsToProcess % cntWorkers != 0);
795
796 constexpr uint32_t maxUnitsOfWorkPerGroup = 8;
797 groupSize = groupSize / maxUnitsOfWorkPerGroup;
798 if (groupSize <= 0)
799 groupSize = 1;
800 }
801
802 const auto jobs = itemsToProcess / groupSize + (itemsToProcess % groupSize != 0);
803
804 if (jobs == 1) {
805 const uint32_t groupJobIdxEnd = groupSize < itemsToProcess ? groupSize : itemsToProcess;
806 auto handle = add(Job{parallel_group_func(job, 0, groupJobIdxEnd), prio, JobCreationFlags::Default});
807 submit(handle);
808 return handle;
809 }
810
811 auto* pHandles = (JobHandle*)alloca(sizeof(JobHandle) * (jobs + 1));
812 std::span<JobHandle> handles(pHandles, jobs + 1);
813
814 add_n(prio, handles);
815
816#if GAIA_ASSERT_ENABLED
817 for (auto jobHandle: handles)
818 GAIA_ASSERT(m_jobManager.is_clear(jobHandle));
819#endif
820
821 for (uint32_t jobIndex = 0; jobIndex < jobs; ++jobIndex) {
822 const uint32_t groupJobIdxStart = jobIndex * groupSize;
823 const uint32_t groupJobIdxEnd =
824 core::get_min(groupSize, itemsToProcess - groupJobIdxStart) + groupJobIdxStart;
825
826 auto& jobData = m_jobManager.data(pHandles[jobIndex]);
827 jobData.func = parallel_group_func(job, groupJobIdxStart, groupJobIdxEnd);
828 jobData.prio = prio;
829 }
830 {
831 auto& jobData = m_jobManager.data(pHandles[jobs]);
832 jobData.prio = prio;
833 }
834
835 dep(handles.subspan(0, jobs), pHandles[jobs]);
836 submit(handles);
837 return pHandles[jobs];
838 }
839
847 GAIA_NODISCARD bool done(JobHandle jobHandle) const {
848 GAIA_ASSERT(main_thread());
849 GAIA_ASSERT(jobHandle != (JobHandle)JobNull_t{});
850
851 const auto& jobData = m_jobManager.data(jobHandle);
852 GAIA_ASSERT(jobData.data.gen == jobHandle.gen());
853 return JobManager::done(jobData);
854 }
855
863 void wait(JobHandle jobHandle) {
864 GAIA_PROF_SCOPE(tp::wait);
865
866 GAIA_ASSERT(main_thread());
867
868 // Skip waiting for unset job handles.
869 if (jobHandle == (JobHandle)JobNull_t{})
870 return;
871
872 auto* ctx = detail::tl_workerCtx;
873 auto& jobData = m_jobManager.data(jobHandle);
874 const bool waitBackground = is_background(jobData);
875 auto state = jobData.state.load(std::memory_order_acquire);
876
877 // Waiting for a job that has not been initialized is nonsense.
878 GAIA_ASSERT(state != 0);
879
880 // Wait until done
881 for (; (state & JobState::STATE_BITS_MASK) < JobState::Done;
882 state = jobData.state.load(std::memory_order_acquire)) {
883 // The job we are waiting for is not finished yet, try running some other job in the meantime
884 JobHandle otherJobHandle;
885 const bool canHelpBackground = waitBackground && m_backgroundWorkersCnt == 0;
886 const bool hasBackgroundJob = canHelpBackground && try_fetch_background_job(otherJobHandle);
887 const bool hasJob = hasBackgroundJob || try_fetch_job(*ctx, otherJobHandle);
888 if (hasJob) {
889 if (run(otherJobHandle, ctx))
890 continue;
891 }
892
893 // The job we are waiting for is already running.
894 // Wait until it signals it's finished.
895 if ((state & JobState::STATE_BITS_MASK) == JobState::Executing) {
896 const auto workerId = (state & JobState::DEP_BITS_MASK);
897 auto* jobDoneEvent = &m_workersCtx[workerId].event;
898 jobDoneEvent->wait();
899 continue;
900 }
901
902 // The worst case scenario.
903 // We have nothing to do and the job we are waiting for is not executing still.
904 // Let's wait for any job to start executing.
905 const auto workerBit = 1U << ctx->workerIdx;
906 const auto oldBlockedMask = m_blockedInWorkUntil.fetch_or(workerBit);
907 const auto newState = jobData.state.load();
908 if (newState == state) // still not JobState::Done?
909 Futex::wait(&m_blockedInWorkUntil, oldBlockedMask | workerBit, detail::WaitMaskAny);
910 m_blockedInWorkUntil.fetch_and(~workerBit);
911 }
912 }
913
916 void update() {
917 GAIA_ASSERT(main_thread());
918 main_thread_tick();
919 }
920
923 GAIA_NODISCARD static uint32_t hw_thread_cnt() {
924 auto hwThreads = (uint32_t)std::thread::hardware_concurrency();
925 return core::get_max(1U, hwThreads);
926 }
927
930 GAIA_NODISCARD static uint32_t hw_efficiency_cores_cnt() {
931 uint32_t efficiencyCores = 0;
932#if GAIA_PLATFORM_APPLE
933 size_t size = sizeof(efficiencyCores);
934 if (sysctlbyname("hw.perflevel1.logicalcpu", &efficiencyCores, &size, nullptr, 0) != 0)
935 return 0;
936#elif GAIA_PLATFORM_FREEBSD
937 int cpuIndex = 0;
938 char oidName[32];
939 int coreType;
940 size_t size = sizeof(coreType);
941 while (true) {
942 GAIA_STRFMT(oidName, sizeof(oidName), "dev.cpu.%d.coretype", cpuIndex);
943 if (sysctlbyname(oidName, &coreType, &size, nullptr, 0) != 0)
944 break; // Stop on the last CPU index
945
946 // 0 = performance core
947 // 1 = efficiency core
948 if (coreType == 1)
949 ++efficiencyCores;
950
951 ++cpuIndex;
952 }
953#elif GAIA_PLATFORM_WINDOWS
954 const auto getProcessorInfo =
955 win32::kernel32_func<win32::GetLogicalProcessorInformationExFunc>("GetLogicalProcessorInformationEx");
956 if (getProcessorInfo == nullptr)
957 return 0;
958
959 unsigned long length = 0;
960
961 // First, determine required buffer size
962 if (!getProcessorInfo(win32::RelationProcessorCore, nullptr, &length))
963 return 0;
964
965 // Allocate enough memory
966 auto* pBuffer = (char*)malloc(length);
967 if (pBuffer == nullptr)
968 return 0;
969
970 // Retrieve the data
971 if (!getProcessorInfo(win32::RelationProcessorCore, pBuffer, &length)) {
972 free(pBuffer);
973 return 0;
974 }
975
976 uint32_t heterogenousCnt = 0;
977
978 // Iterate over processor core entries.
979 // On Windows we can't directly tell whether a core is an efficiency core or a performance core.
980 // Instead:
981 // - lower EfficiencyClass values correspond to more efficient cores
982 // - higher EfficiencyClass values correspond to higher performance cores
983 // - EfficiencyClass is zero for homogeneous CPU architectures
984 // Therefore, to count efficiency cores on Windows, we will count cores where EfficiencyClass == 0.
985 // On heterogeneous this should gives us the correct results.
986 // On homogenous architectures, the value is always 0 so rather than calculating the number of efficiency
987 // cores, we would calculate the number of performance cores. For the sake of correctness, if all cores return
988 // 0, we use 0 for the number of efficiency cores.
989 // Each SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX entry starts with its relationship and its size.
990 for (const char* ptr = pBuffer; ptr < pBuffer + length;) {
991 int relationship = 0;
992 unsigned long size = 0;
993 memcpy(&relationship, ptr, sizeof(relationship));
994 memcpy(&size, ptr + sizeof(relationship), sizeof(size));
995 if (size == 0)
996 break;
997 if (relationship == win32::RelationProcessorCore) {
998 const auto efficiencyClass = (uint8_t)ptr[win32::ProcessorInfoEfficiencyClassOffset];
999 if (efficiencyClass == 0)
1000 ++efficiencyCores;
1001 else
1002 ++heterogenousCnt;
1003 }
1004 ptr += size;
1005 }
1006
1007 if (heterogenousCnt == 0)
1008 efficiencyCores = 0;
1009
1010 free(pBuffer);
1011#elif GAIA_PLATFORM_LINUX
1012 {
1013 // Intel has /sys/devices/cpu_core/cpus, /sys/devices/cpu_atom/cpus on some systems
1014 DIR* dir = opendir("/sys/devices/cpu_atom/cpus/");
1015 if (dir == nullptr)
1016 return 0;
1017
1018 dirent* entry;
1019 while ((entry = readdir(dir)) != nullptr) {
1020 if (strncmp(entry->d_name, "cpu", 3) == 0 && entry->d_name[3] >= '0' && entry->d_name[3] <= '9')
1021 ++efficiencyCores;
1022 }
1023
1024 closedir(dir);
1025 }
1026
1027 if (efficiencyCores == 0) {
1028 // TODO: Go through all CPUs packages and CPUs and determine the differences between them.
1029 // There are many metrics.
1030 // 1) We will assume all CPUs to be of the same architecture.
1031 // 2) Same CPU architecture but different cache sizes. Smaller ones are "efficiency" cores.
1032 // This is the AMD way. Still, these are about the same things so maybe we would just treat
1033 // all such cores as performance cores.
1034 // 3) Different max frequencies on different cores. This might be indicative enough.
1035 // There is also an optional parameter present on ARM CPUs:
1036 // https://www.kernel.org/doc/Documentation/devicetree/bindings/arm/cpu-capacity.txt
1037 // In this case, we'd treat CPUs with the highest capacity-dmips-mhz as performance cores,
1038 // and consider the rest as efficiency cores.
1039
1040 // ...
1041 }
1042#endif
1043 return efficiencyCores;
1044 }
1045
1046 private:
1047 static void* thread_func(void* pCtx) {
1048 auto& ctx = *(ThreadCtx*)pCtx;
1049
1050 detail::tl_workerCtx = &ctx;
1051
1052 // Set the worker thread name.
1053 // Needs to be called from inside the thread because some platforms
1054 // can change the name only when run from the specific thread.
1055 ctx.tp->set_thread_name(ctx.workerIdx, ctx.prio);
1056
1057 // Set the worker thread priority
1058 ctx.tp->set_thread_priority(ctx.workerIdx, ctx.prio);
1059
1060 // Process jobs
1061 ctx.tp->worker_loop(ctx);
1062
1063 detail::tl_workerCtx = nullptr;
1064
1065 return nullptr;
1066 }
1067
1072 void create_thread(uint32_t workerIdx, JobPriority prio, bool background) {
1073 // Idx 0 is reserved for the main thread
1074 GAIA_ASSERT(workerIdx > 0);
1075
1076 auto& ctx = m_workersCtx[workerIdx];
1077 ctx.tp = this;
1078 ctx.workerIdx = workerIdx;
1079 ctx.prio = prio;
1080 ctx.background = background;
1081 ctx.threadCreated = false;
1082
1083#if GAIA_THREAD_PLATFORM == GAIA_THREAD_STD
1084 m_workers[workerIdx - 1] = std::thread([&ctx]() {
1085 thread_func((void*)&ctx);
1086 });
1087#else
1088 pthread_attr_t attr{};
1089 int ret = pthread_attr_init(&attr);
1090 if (ret != 0) {
1091 GAIA_LOG_W("pthread_attr_init failed for worker thread %u. ErrCode = %d", workerIdx, ret);
1092 return;
1093 }
1094
1096 // Apple's recommendation for Apple Silicon for games / high-perf software
1097 // ========================================================================
1098 // Per frame | Scheduling policy | QoS class / Priority
1099 // ========================================================================
1100 // Main thread | SCHED_OTHER | QOS_CLASS_USER_INTERACTIVE (47)
1101 // Render/Audio thread | SCHED_RR | 45
1102 // Workers High Prio | SCHED_RR | 39-41
1103 // Workers Low Prio | SCHED_OTHER | QOS_CLASS_USER_INTERACTIVE (38)
1104 // ========================================================================
1105 // Multiple-frames | |
1106 // ========================================================================
1107 // Async Workers High Prio| SCHED_OTHER | QOS_CLASS_USER_INITIATED (37)
1108 // Async Workers Low Prio | SCHED_OTHER | QOS_CLASS_DEFAULT (31)
1109 // Prefetching/Streaming | SCHED_OTHER | QOS_CLASS_UTILITY (20)
1110 // ========================================================================
1111
1112 if (prio == JobPriority::Low) {
1113 #if GAIA_PLATFORM_APPLE
1114 ret = pthread_attr_set_qos_class_np(&attr, QOS_CLASS_USER_INTERACTIVE, -9); // 47-9=38
1115 if (ret != 0) {
1116 GAIA_LOG_W(
1117 "pthread_attr_set_qos_class_np failed for worker thread %u [prio=%u]. ErrCode = %d", workerIdx,
1118 (uint32_t)prio, ret);
1119 }
1120 #else
1121 ret = pthread_attr_setschedpolicy(&attr, SCHED_OTHER);
1122 if (ret != 0) {
1123 GAIA_LOG_W(
1124 "pthread_attr_setschedpolicy SCHED_RR failed for worker thread %u [prio=%u]. ErrCode = %d", workerIdx,
1125 (uint32_t)prio, ret);
1126 }
1127
1128 int prioMax = core::get_min(38, sched_get_priority_max(SCHED_OTHER));
1129 int prioMin = core::get_min(prioMax, sched_get_priority_min(SCHED_OTHER));
1130 int prioUse = core::get_min(prioMin + 5, prioMax);
1131 prioUse = core::get_max(prioUse, prioMin);
1132 sched_param param{};
1133 param.sched_priority = prioUse;
1134
1135 ret = pthread_attr_setschedparam(&attr, &param);
1136 if (ret != 0) {
1137 GAIA_LOG_W(
1138 "pthread_attr_setschedparam %d failed for worker thread %u [prio=%u]. ErrCode = %d",
1139 param.sched_priority, workerIdx, (uint32_t)prio, ret);
1140 }
1141 #endif
1142 } else {
1143 ret = pthread_attr_setschedpolicy(&attr, SCHED_RR);
1144 if (ret != 0) {
1145 GAIA_LOG_W(
1146 "pthread_attr_setschedpolicy SCHED_RR failed for worker thread %u [prio=%u]. ErrCode = %d", workerIdx,
1147 (uint32_t)prio, ret);
1148 }
1149
1150 int prioMax = core::get_min(41, sched_get_priority_max(SCHED_RR));
1151 int prioMin = core::get_min(prioMax, sched_get_priority_min(SCHED_RR));
1152 int prioUse = core::get_max(prioMax - 5, prioMin);
1153 prioUse = core::get_min(prioUse, prioMax);
1154 sched_param param{};
1155 param.sched_priority = prioUse;
1156
1157 ret = pthread_attr_setschedparam(&attr, &param);
1158 if (ret != 0) {
1159 GAIA_LOG_W(
1160 "pthread_attr_setschedparam %d failed for worker thread %u [prio=%u]. ErrCode = %d",
1161 param.sched_priority, workerIdx, (uint32_t)prio, ret);
1162 }
1163 }
1164
1165 // Create the thread with given attributes
1166 ret = pthread_create(&m_workers[workerIdx - 1], &attr, thread_func, (void*)&ctx);
1167 if (ret != 0) {
1168 GAIA_LOG_W("pthread_create failed for worker thread %u. ErrCode = %d", workerIdx, ret);
1169 } else {
1170 ctx.threadCreated = true;
1171 }
1172
1173 pthread_attr_destroy(&attr);
1174#endif
1175
1176 // Stick each thread to a specific CPU core if possible
1177 set_thread_affinity(workerIdx);
1178 }
1179
1182 void join_thread(uint32_t workerIdx) {
1183 if GAIA_UNLIKELY (workerIdx > m_workers.size())
1184 return;
1185
1186#if GAIA_THREAD_PLATFORM == GAIA_THREAD_STD
1187 auto& t = m_workers[workerIdx - 1];
1188 if (t.joinable())
1189 t.join();
1190#else
1191 auto& ctx = m_workersCtx[workerIdx];
1192 if (!ctx.threadCreated)
1193 return;
1194
1195 auto& t = m_workers[workerIdx - 1];
1196 pthread_join(t, nullptr);
1197 ctx.threadCreated = false;
1198#endif
1199 }
1200
1201 void create_worker_threads(uint32_t& workerIdx, JobPriority prio, uint32_t count) {
1202 for (uint32_t i = 0; i < count; ++i)
1203 create_thread(workerIdx++, prio, false);
1204 }
1205
1206 void create_background_worker_threads(uint32_t& workerIdx) {
1207 for (uint32_t i = 0; i < m_backgroundWorkersCnt; ++i)
1208 create_thread(workerIdx++, JobPriority::Low, true);
1209 }
1210
1211 void set_thread_priority([[maybe_unused]] uint32_t workerIdx, [[maybe_unused]] JobPriority priority) {
1212#if GAIA_PLATFORM_WINDOWS
1213 void* nativeHandle = (void*)m_workers[workerIdx - 1].native_handle();
1214
1215 const auto setThreadInfo = win32::kernel32_func<win32::SetThreadInformationFunc>("SetThreadInformation");
1216 if (setThreadInfo == nullptr)
1217 return;
1218
1219 win32::ThreadPowerThrottlingState state{};
1220 state.Version = win32::ThreadPowerThrottlingCurrentVersion;
1221 if (priority == JobPriority::High) {
1222 // HighQoS
1223 // Turn EXECUTION_SPEED throttling off.
1224 // ControlMask selects the mechanism and StateMask is set to zero as mechanisms should be turned off.
1225 state.ControlMask = win32::ThreadPowerThrottlingExecutionSpeed;
1226 state.StateMask = 0;
1227 } else {
1228 // EcoQoS
1229 // Turn EXECUTION_SPEED throttling on.
1230 // ControlMask selects the mechanism and StateMask declares which mechanism should be on or off.
1231 state.ControlMask = win32::ThreadPowerThrottlingExecutionSpeed;
1232 state.StateMask = win32::ThreadPowerThrottlingExecutionSpeed;
1233 }
1234
1235 const int ret = setThreadInfo(nativeHandle, win32::ThreadPowerThrottling, &state, sizeof(state));
1236 if (ret == 0) {
1237 GAIA_LOG_W("SetThreadInformation failed for thread %u", workerIdx);
1238 return;
1239 }
1240#else
1241 // Done when the thread is created
1242#endif
1243 }
1244
1245 void set_thread_affinity([[maybe_unused]] uint32_t workerIdx) {
1246 // NOTE:
1247 // Some cores might have multiple logic threads, there might be
1248 // more sockets and some cores might even be physically different
1249 // form others (performance vs efficiency cores).
1250 // Because of that, do not handle affinity and let the OS figure it out.
1251 // All treads created by the pool are setting thread priorities to make
1252 // it easier for the OS.
1253
1254 // #if GAIA_PLATFORM_WINDOWS
1255 // HANDLE nativeHandle = (HANDLE)m_workers[workerIdx-1].native_handle();
1256 //
1257 // auto mask = SetThreadAffinityMask(nativeHandle, 1ULL << workerIdx);
1258 // if (mask <= 0)
1259 // GAIA_LOG_W("Issue setting thread affinity for worker thread %u!", workerIdx);
1260 // #elif GAIA_PLATFORM_APPLE
1261 // // Do not do affinity for MacOS. If is not supported for Apple Silicon and
1262 // // Intel MACs are deprecated anyway.
1263 // // TODO: Consider supporting this at least for Intel MAC as there are still
1264 // // quite of few of them out there.
1265 // #elif GAIA_PLATFORM_LINUX || GAIA_PLATFORM_FREEBSD
1266 // pthread_t nativeHandle = (pthread_t)m_workers[workerIdx-1].native_handle();
1267 //
1268 // cpu_set_t cpuSet;
1269 // CPU_ZERO(&cpuSet);
1270 // CPU_SET(workerIdx, &cpuSet);
1271 //
1272 // auto ret = pthread_setaffinity_np(nativeHandle, sizeof(cpuSet), &cpuSet);
1273 // if (ret != 0)
1274 // GAIA_LOG_W("Issue setting thread affinity for worker thread %u!", workerIdx);
1275 //
1276 // ret = pthread_getaffinity_np(nativeHandle, sizeof(cpuSet), &cpuSet);
1277 // if (ret != 0)
1278 // GAIA_LOG_W("Thread affinity could not be set for worker thread %u!", workerIdx);
1279 // #endif
1280 }
1281
1285 void set_thread_name(uint32_t workerIdx, JobPriority prio) {
1286 const bool background = m_workersCtx[workerIdx].background;
1287 const char* workerKind = background ? "BG" : prio == JobPriority::High ? "HI" : "LO";
1288#if GAIA_PROF_USE_PROFILER_THREAD_NAME
1289 char threadName[16]{};
1290 GAIA_STRFMT(threadName, 16, "worker_%s_%u", workerKind, workerIdx);
1291 GAIA_PROF_THREAD_NAME(threadName);
1292#elif GAIA_PLATFORM_WINDOWS
1293 void* nativeHandle = (void*)m_workers[workerIdx - 1].native_handle();
1294 const wchar_t* workerKindW = background ? L"BG" : prio == JobPriority::High ? L"HI" : L"LO";
1295
1296 const auto pSetThreadDescFunc = win32::kernel32_func<win32::SetThreadDescriptionFunc>("SetThreadDescription");
1297 if (pSetThreadDescFunc != nullptr) {
1298 wchar_t threadName[16]{};
1299 swprintf_s(threadName, L"worker_%s_%u", workerKindW, workerIdx);
1300
1301 const long hr = pSetThreadDescFunc(nativeHandle, threadName);
1302 if (hr < 0) {
1303 GAIA_LOG_W("Issue setting name for worker %s thread %u!", workerKind, workerIdx);
1304 }
1305 }
1306#elif GAIA_PLATFORM_APPLE
1307 char threadName[16]{};
1308 GAIA_STRFMT(threadName, 16, "worker_%s_%u", workerKind, workerIdx);
1309 auto ret = pthread_setname_np(threadName);
1310 if (ret != 0)
1311 GAIA_LOG_W("Issue setting name for worker %s thread %u!", workerKind, workerIdx);
1312#elif GAIA_PLATFORM_LINUX || GAIA_PLATFORM_FREEBSD
1313 auto nativeHandle = m_workers[workerIdx - 1];
1314
1315 // Linux limits thread names to 15 characters plus the terminator. Format the full name
1316 // and cut it explicitly, so the limit is not left to a silently truncating snprintf.
1317 char threadName[32]{};
1318 GAIA_STRFMT(threadName, 32, "worker_%s_%u", workerKind, workerIdx);
1319 GAIA_PROF_THREAD_NAME(threadName);
1320 threadName[15] = 0;
1321 auto ret = pthread_setname_np(nativeHandle, threadName);
1322 if (ret != 0)
1323 GAIA_LOG_W("Issue setting name for worker %s thread %u!", workerKind, workerIdx);
1324#endif
1325 }
1326
1329 GAIA_NODISCARD bool main_thread() const {
1330 return std::this_thread::get_id() == m_mainThreadId;
1331 }
1332
1335 void main_thread_tick() {
1336 auto& ctx = *detail::tl_workerCtx;
1337
1338 // Keep executing while there is work
1339 while (true) {
1340 JobHandle jobHandle;
1341 if (!try_fetch_job(ctx, jobHandle))
1342 break;
1343
1344 (void)run(jobHandle, &ctx);
1345 }
1346 }
1347
1353 GAIA_NODISCARD bool try_steal_job(ThreadCtx& ctx, JobPriority prio, JobHandle& jobHandle) {
1354 const auto workerCnt = m_workersCtx.size();
1355 for (uint32_t i = 0; i < workerCnt;) {
1356 // Keep stealing within the same priority class and skip our own queue
1357 if (i == ctx.workerIdx || m_workersCtx[i].background || m_workersCtx[i].prio != prio) {
1358 ++i;
1359 continue;
1360 }
1361
1362 const auto res = m_workersCtx[i].jobQueue.try_steal(jobHandle);
1363 // Race condition, try again from the same context
1364 if (!res)
1365 continue;
1366
1367 // Stealing can return true if the queue is empty.
1368 // We return right away only if we receive a valid handle which means
1369 // when there was an idle job in the queue.
1370 if (jobHandle != (JobHandle)JobNull_t{})
1371 return true;
1372
1373 ++i;
1374 }
1375
1376 return false;
1377 }
1378
1384 GAIA_NODISCARD bool try_fetch_prio(ThreadCtx& ctx, JobPriority prio, JobHandle& jobHandle) {
1385 if (m_jobQueue[(uint32_t)prio].try_pop(jobHandle))
1386 return true;
1387
1388 return try_steal_job(ctx, prio, jobHandle);
1389 }
1390
1394 GAIA_NODISCARD bool try_fetch_background_job(JobHandle& jobHandle) {
1395 return m_jobQueueBackground.try_pop(jobHandle);
1396 }
1397
1402 GAIA_NODISCARD bool try_fetch_job(ThreadCtx& ctx, JobHandle& jobHandle) {
1403 if (ctx.background)
1404 return try_fetch_background_job(jobHandle);
1405
1406 // Try getting a job from the local queue
1407 if (ctx.jobQueue.try_pop(jobHandle))
1408 return true;
1409
1410 // The main thread may help with both queues while waiting or updating
1411 if (ctx.workerIdx == 0) {
1412 if (try_fetch_prio(ctx, JobPriority::High, jobHandle))
1413 return true;
1414
1415 return try_fetch_prio(ctx, JobPriority::Low, jobHandle);
1416 }
1417
1418 return try_fetch_prio(ctx, ctx.prio, jobHandle);
1419 }
1420
1425 GAIA_NODISCARD bool can_run_inline(const ThreadCtx* ctx, const JobContainer& jobData) const {
1426 const bool background = is_background(jobData);
1427 if (background)
1428 return (ctx != nullptr && ctx->background) || m_backgroundWorkersCnt == 0;
1429
1430 // The main thread is allowed to help with both priority classes.
1431 if (ctx == nullptr || ctx->workerIdx == 0)
1432 return true;
1433
1434 // Matching worker classes may execute their own overflow inline.
1435 if (!ctx->background && ctx->prio == jobData.prio)
1436 return true;
1437
1438 // If there are no spawned workers for the target priority we need to preserve
1439 // the forward-progress guarantee and allow inline fallback.
1440 return m_workerThreadsCnt[(uint32_t)jobData.prio] == 0;
1441 }
1442
1446 void wait_for_queue_space(ThreadCtx& ctx, const JobContainer& jobData) {
1447 const bool background = is_background(jobData);
1448
1449 // Wake one worker from the target class in case all of them are asleep while
1450 // the producer is waiting for queue space to become available.
1451 if (background) {
1452 if (m_backgroundWorkersCnt != 0)
1453 m_semBackground.release(1);
1454 } else {
1455 const auto prioIdx = (uint32_t)jobData.prio;
1456 if (m_workerThreadsCnt[prioIdx] != 0)
1457 m_sem[prioIdx].release(1);
1458 }
1459
1460 // Keep the current worker productive without violating the priority boundary.
1461 JobHandle otherJobHandle;
1462 const bool hasWork =
1463 ctx.background ? try_fetch_background_job(otherJobHandle) : try_fetch_prio(ctx, ctx.prio, otherJobHandle);
1464 if (hasWork) {
1465 (void)run(otherJobHandle, &ctx);
1466 return;
1467 }
1468
1469 std::this_thread::yield();
1470 }
1471
1475 void worker_loop(ThreadCtx& ctx) {
1476 while (true) {
1477 // Wait for work
1478 if (ctx.background)
1479 m_semBackground.wait();
1480 else
1481 m_sem[(uint32_t)ctx.prio].wait();
1482
1483 // Keep executing while there is work
1484 while (true) {
1485 JobHandle jobHandle;
1486 if (!try_fetch_job(ctx, jobHandle))
1487 break;
1488
1489 (void)run(jobHandle, detail::tl_workerCtx);
1490 }
1491
1492 // Check if the worker can keep running
1493 const bool stop = m_stop.load();
1494 if (stop)
1495 break;
1496 }
1497 }
1498
1500 void reset() {
1501 if (m_workers.empty())
1502 return;
1503
1504 // Request stopping
1505 m_stop.store(true);
1506
1507 // Signal all threads
1508 GAIA_FOR(JobPriorityCnt) {
1509 if (m_workerThreadsCnt[i] != 0)
1510 m_sem[i].release((int32_t)m_workerThreadsCnt[i]);
1511 }
1512 if (m_backgroundWorkersCnt != 0)
1513 m_semBackground.release((int32_t)m_backgroundWorkersCnt);
1514
1515 auto* ctx = detail::tl_workerCtx;
1516 if (ctx == nullptr) {
1517 // reset() can be reached during static teardown from a thread that never
1518 // entered the pool and therefore has no TLS worker context bound.
1519 ctx = &m_workersCtx[0];
1520 detail::tl_workerCtx = ctx;
1521 }
1522
1523 // Finish remaining jobs
1524 JobHandle jobHandle;
1525 while (try_fetch_job(*ctx, jobHandle)) {
1526 run(jobHandle, ctx);
1527 }
1528 while (try_fetch_background_job(jobHandle)) {
1529 run(jobHandle, ctx);
1530 }
1531
1532 detail::tl_workerCtx = nullptr;
1533
1534 // Join threads with the main one
1535 GAIA_FOR(m_workers.size()) join_thread(i + 1);
1536
1537 // All threads have been stopped. Allow new threads to run if necessary.
1538 m_stop.store(false);
1539 }
1540
1542 JobPriority final_frame_prio(JobPriority priority) {
1543 const auto cntWorkers = m_workersCnt[(uint32_t)priority];
1544 return cntWorkers > 0
1545 // If there is enough workers, keep the priority
1546 ? priority
1547 // Not enough workers, use the other priority that has workers
1548 : (JobPriority)(((uint32_t)priority + 1U) % (uint32_t)JobPriorityCnt);
1549 }
1550
1552 JobPriority final_prio(const Job& job) {
1553 if ((job.flags & JobCreationFlags::Background) != 0U)
1554 return job.priority;
1555
1556 return final_frame_prio(job.priority);
1557 }
1558
1560 template <typename TJob>
1561 JobPriority final_prio(const TJob& job) {
1562 return final_frame_prio(job.priority);
1563 }
1564
1568 GAIA_NODISCARD static bool is_background(const JobContainer& jobData) {
1569 return (jobData.flags & JobCreationFlags::Background) != 0U;
1570 }
1571
1575 static void copy_edges(const JobContainer& jobData, JobHandle* pEdges) {
1576 const auto cnt = jobData.edges.depCnt;
1577 if (cnt == 1) {
1578 pEdges[0] = jobData.edges.dep;
1579 return;
1580 }
1581 GAIA_FOR(cnt) pEdges[i] = jobData.edges.pDeps[i];
1582 }
1583
1588 uint32_t signal_edges(std::span<const JobHandle> edges, JobHandle* pReadyHandles) {
1589 uint32_t cnt = 0;
1590 for (const auto depHandle: edges) {
1591#if GAIA_LOG_JOB_STATES
1592 GAIA_LOG_N("SIGNAL -> %u.%u", depHandle.id(), depHandle.gen());
1593#endif
1594 auto& depData = m_jobManager.data(depHandle);
1595 if (!JobManager::signal_edge(depData))
1596 continue;
1597
1598 pReadyHandles[cnt++] = depHandle;
1599 }
1600
1601 return cnt;
1602 }
1603
1607 void process(std::span<JobHandle> jobHandles, ThreadCtx* ctx) {
1608 auto* pHandles = (JobHandle*)alloca(sizeof(JobHandle) * jobHandles.size());
1609 uint32_t handlesCnt = 0;
1610
1611 for (auto handle: jobHandles) {
1612 auto& jobData = m_jobManager.data(handle);
1613 m_jobManager.processing(jobData);
1614
1615 // Jobs that have no functor assigned don't need to be enqueued.
1616 // We can "run" them right away. The only time where it makes
1617 // sense to create such a job is to create a sync job. E.g. when you
1618 // need to wait for N jobs, rather than waiting for each of them
1619 // separately you make them a dependency of a dummy/sync job and
1620 // wait just for that one.
1621 if (!jobData.func.operator bool())
1622 (void)run(handle, ctx);
1623 else
1624 pHandles[handlesCnt++] = handle;
1625 }
1626
1627 std::span handles(pHandles, handlesCnt);
1628 while (!handles.empty()) {
1629 // Try pushing all jobs while preserving their priority queue ownership.
1630 uint32_t pushed = 0;
1631 uint32_t released[JobPriorityCnt]{};
1632 uint32_t backgroundReleased = 0;
1633 for (; pushed < handles.size(); ++pushed) {
1634 const auto handle = handles[pushed];
1635 const auto& jobData = m_jobManager.data(handle);
1636 if (is_background(jobData)) {
1637 if (!m_jobQueueBackground.try_push(handle))
1638 break;
1639
1640 ++backgroundReleased;
1641 continue;
1642 }
1643
1644 const auto prio = jobData.prio;
1645 // Worker-local queues are reserved for work that matches the worker's own
1646 // priority class. Cross-priority releases must go through the matching
1647 // global queue so the right workers can pick them up.
1648 const bool useLocalQueue = ctx != nullptr && !ctx->background && ctx->workerIdx != 0 && ctx->prio == prio;
1649 const bool res =
1650 useLocalQueue ? ctx->jobQueue.try_push(handle) : m_jobQueue[(uint32_t)prio].try_push(handle);
1651 if (!res)
1652 break;
1653
1654 released[(uint32_t)prio]++;
1655 }
1656
1657 GAIA_FOR(JobPriorityCnt) {
1658 // Only spawned worker threads block on semaphores. The main thread helps by
1659 // draining queues opportunistically from wait() and update().
1660 const auto cnt = core::get_min(released[i], m_workerThreadsCnt[i]);
1661 if (cnt != 0)
1662 m_sem[i].release((int32_t)cnt);
1663 }
1664 const auto backgroundCnt = core::get_min(backgroundReleased, m_backgroundWorkersCnt);
1665 if (backgroundCnt != 0)
1666 m_semBackground.release((int32_t)backgroundCnt);
1667
1668 handles = handles.subspan(pushed);
1669 if (!handles.empty()) {
1670 const auto handle = handles[0];
1671 const auto& jobData = m_jobManager.data(handle);
1672
1673 if (can_run_inline(ctx, jobData)) {
1674 // The queue was full. Execute the job right away only when the
1675 // current execution context is allowed to run this priority class.
1676 run(handle, ctx);
1677 handles = handles.subspan(1);
1678 } else {
1679 GAIA_ASSERT(ctx != nullptr);
1680 wait_for_queue_space(*ctx, jobData);
1681 }
1682 }
1683 }
1684 }
1685
1686 bool run(JobHandle jobHandle, ThreadCtx* ctx) {
1687 if (jobHandle == (JobHandle)JobNull_t{})
1688 return false;
1689
1690 auto& jobData = m_jobManager.data(jobHandle);
1691 const bool manualDelete = (jobData.flags & JobCreationFlags::ManualDelete) != 0U;
1692 const bool canWait = (jobData.flags & JobCreationFlags::CanWait) != 0U;
1693
1694 m_jobManager.executing(jobData, ctx->workerIdx);
1695
1696 if (m_blockedInWorkUntil.load() != 0) {
1697 const auto blockedCnt = m_blockedInWorkUntil.exchange(0);
1698 if (blockedCnt != 0)
1699 Futex::wake(&m_blockedInWorkUntil, detail::WaitMaskAll);
1700 }
1701
1702 GAIA_ASSERT(jobData.idx != (uint32_t)-1 && jobData.data.gen != (uint32_t)-1);
1703
1704 // Run the functor associated with the job
1705 m_jobManager.run(jobData);
1706
1707 // An automatically deleted job can never run again. End its callable's lifetime, and that of everything it
1708 // captured, now on this thread, so nobody can see the job done while its captures are still alive. Only a
1709 // callable with a non-trivial destructor has anything to end. Its storage is released with the job slot,
1710 // after Done, which keeps the work between the job body and Done to a minimum.
1711 if (!manualDelete)
1712 jobData.func.finish();
1713
1714 if (jobData.edges.depCnt == 0) {
1715 test_before_finalize(jobHandle);
1716 JobManager::finalize(jobData);
1717 } else {
1718 // Copy the outgoing edges first. Once Done is published, whoever owns this job may delete it,
1719 // which frees the edge storage and recycles the slot.
1720 const auto edgeCnt = jobData.edges.depCnt;
1721 auto* pEdges = (JobHandle*)alloca(sizeof(JobHandle) * edgeCnt);
1722 copy_edges(jobData, pEdges);
1723 if (!manualDelete)
1724 JobManager::free_edges(jobData);
1725
1726 // Publish completion before signaling dependents. A dependent made ready here can run and
1727 // finish, and whoever waits for it may then delete this job (e.g. the children of a parallel
1728 // job once its sync job is done). This job must already be Done by then.
1729 test_before_finalize(jobHandle);
1730 JobManager::finalize(jobData);
1731
1732 const auto readyCnt = signal_edges(std::span<const JobHandle>(pEdges, edgeCnt), pEdges);
1733 process(std::span(pEdges, readyCnt), ctx);
1734 }
1735
1736 // Signal we finished
1737 ctx->event.set();
1738 if (canWait) {
1739 const auto* pFutexValue = &jobData.state;
1740 Futex::wake(pFutexValue, detail::WaitMaskAll);
1741 }
1742
1743 if (!manualDelete)
1744 release_job(jobHandle);
1745
1746 return true;
1747 }
1748 };
1749
1750 GAIA_MSVC_WARNING_POP()
1751 } // namespace mt
1752} // namespace gaia
Array of elements of type.
Definition darray_ext_impl.h:31
GAIA_NODISCARD size_type size() const noexcept
Returns the number of elements.
Definition darray_ext_impl.h:561
void resize(size_type count, ResizePolicy policy=ResizePolicy::Exact)
Changes the number of elements.
Definition darray_ext_impl.h:289
GAIA_NODISCARD pointer data() noexcept
Returns a pointer to the element storage.
Definition darray_ext_impl.h:236
GAIA_NODISCARD bool empty() const noexcept
Checks whether the container has no elements.
Definition darray_ext_impl.h:567
An optimized version of Semaphore that avoids expensive system calls when the counter is greater than...
Definition semaphore_fast.h:12
bool wait()
Decrements semaphore count by 1. If the count is already 0, it waits indefinitely until semaphore cou...
Definition semaphore_fast.h:43
void release(int32_t count=1)
Increments semaphore count by the specified amount.
Definition semaphore_fast.h:29
Non-recursive spin lock backed by an atomic flag.
Definition spinlock.h:9
Process-wide worker pool for dependent frame and background jobs.
Definition threadpool.h:78
void set_workers_high_prio_inter(uint32_t &workerIdx, uint32_t count)
Updates the number of worker threads participating at high priority workloads.
Definition threadpool.h:244
JobHandle add(TJob job)
Creates a threadpool job from job.
Definition threadpool.h:391
GAIA_NODISCARD uint32_t background_workers() const
Returns the number of background worker threads.
Definition threadpool.h:192
JobHandle sched_par(JobParallelRef job, uint32_t itemsToProcess, uint32_t groupSize)
Schedules a non-owning parallel job descriptor on worker threads.
Definition threadpool.h:778
void update()
Uses the main thread to help with frame job processing. Background jobs are intentionally excluded fr...
Definition threadpool.h:916
void reset(std::span< JobHandle > jobHandles)
Waits for jobHandles to finish and resets them to a reusable state.
Definition threadpool.h:596
JobHandle sched_par(JobParallel job, uint32_t itemsToProcess, uint32_t groupSize)
Schedules a job to run on worker threads in parallel.
Definition threadpool.h:676
void dep(JobHandle jobFirst, JobHandle jobSecond)
Makes jobSecond depend on jobFirst. This means jobSecond will not run until jobFirst finishes.
Definition threadpool.h:340
void set_max_workers(uint32_t count, uint32_t countHighPrio)
Set the maximum number of frame execution contexts for this system.
Definition threadpool.h:202
void submit(JobHandle jobHandle)
Pushes jobHandle into the internal queue so worker threads can pick it up and execute it....
Definition threadpool.h:557
void set_background_workers(uint32_t count)
Updates the number of worker threads dedicated to background jobs. Background workers run jobs submit...
Definition threadpool.h:301
void dep_refresh(std::span< JobHandle > jobsFirst, JobHandle jobSecond)
Makes jobSecond depend on the jobs listed in jobsFirst. This means jobSecond will not run until all j...
Definition threadpool.h:377
void dep_refresh(JobHandle jobFirst, JobHandle jobSecond)
Makes jobSecond depend on jobFirst. This means jobSecond will not run until jobFirst finishes.
Definition threadpool.h:364
static ThreadPool & get()
Returns the process-wide thread-pool instance.
Definition threadpool.h:168
JobHandle sched(Job job)
Schedules a job to run on a worker thread.
Definition threadpool.h:635
void reset_state(JobHandle jobHandle)
Resets a completed job to the clear reusable state without waiting.
Definition threadpool.h:589
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
JobHandle sched(Job job, JobHandle dependsOn)
Schedules a job to run on a worker thread.
Definition threadpool.h:662
void reset_state(std::span< JobHandle > jobHandles)
Resets completed jobs to the clear reusable state without waiting.
Definition threadpool.h:575
static GAIA_NODISCARD uint32_t hw_efficiency_cores_cnt()
Returns the number of efficiency cores of the system.
Definition threadpool.h:930
void make_main_thread()
Make the calling thread the effective main thread from the thread pool perspective.
Definition threadpool.h:178
void wait(JobHandle jobHandle)
Wait until a job associated with the jobHandle finishes executing. Cleans up any job allocations and ...
Definition threadpool.h:863
void set_workers_low_prio(uint32_t count)
Updates the number of worker threads participating at low priority workloads.
Definition threadpool.h:286
void del(JobHandle jobHandle)
Deletes a job handle jobHandle from the threadpool. The job's callable, and everything it captured,...
Definition threadpool.h:493
void reset(JobHandle jobHandle)
Waits for jobHandle to finish and resets it to a reusable state.
Definition threadpool.h:626
void set_workers_high_prio(uint32_t count)
Updates the number of worker threads participating at high priority workloads.
Definition threadpool.h:274
GAIA_NODISCARD uint32_t workers() const
Returns the number of frame worker threads.
Definition threadpool.h:186
void dep(std::span< JobHandle > jobsFirst, JobHandle jobSecond)
Makes jobSecond depend on the jobs listed in jobsFirst. This means jobSecond will not run until all j...
Definition threadpool.h:351
GAIA_NODISCARD bool done(JobHandle jobHandle) const
Checks whether jobHandle finished executing, without waiting and without running any job....
Definition threadpool.h:847
void set_workers_low_prio_inter(uint32_t &workerIdx, uint32_t count)
Updates the number of worker threads participating at low priority workloads.
Definition threadpool.h:260
JobHandle sched_background(Job job)
Schedules a job to run on background workers. Background jobs are not drained by update() and may spa...
Definition threadpool.h:649
static GAIA_NODISCARD uint32_t hw_thread_cnt()
Returns the number of HW threads available on the system. 1 is minimum.
Definition threadpool.h:923
RAII helper that calls lock() on construction and unlock() on destruction.
Definition utility.h:187
static Result wait(const std::atomic_uint32_t *pFutexValue, uint32_t expected, uint32_t waitMask)
Suspends the caller on the futex while its value remains expected.
Definition futex.h:73
static uint32_t wake(const std::atomic_uint32_t *pFutexValue, uint32_t wakeCount, uint32_t wakeMask=detail::WaitMaskAny)
Wakes up to wakeCount waiters whose waitMask matches wakeMask.
Definition futex.h:106
Half-open item range passed to a parallel job callback.
Definition jobcommon.h:51
uint32_t idxStart
First item index processed by this invocation.
Definition jobcommon.h:53
uint32_t idxEnd
One-past-the-last item index processed by this invocation.
Definition jobcommon.h:55
Packed identifier for a job-pool slot, generation, and priority.
Definition jobhandle.h:17
GAIA_NODISCARD auto gen() const
Returns the slot generation.
Definition jobhandle.h:103
Sentinel type representing an invalid job handle.
Definition jobhandle.h:119
Non-owning callback descriptor for parallel jobs.
Definition jobcommon.h:363
void(* invoke)(void *, const JobArgs &)
Function that invokes the callback stored in the context.
Definition jobcommon.h:367
void * pCtx
Non-owning callback context.
Definition jobcommon.h:365
JobPriority priority
Queue priority used for each range job.
Definition jobcommon.h:369
Callable and priority for a range-partitioned parallel job.
Definition jobcommon.h:354
JobPriority priority
Queue priority used for each range job.
Definition jobcommon.h:358
JobArgsFunc func
Callable invoked once for each scheduled range.
Definition jobcommon.h:356
Callable and scheduling options for a single job.
Definition jobcommon.h:340
JobCreationFlags flags
Creation and lifetime options.
Definition jobcommon.h:350
Per-thread execution state owned by ThreadPool.
Definition jobcommon.h:375