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 <windows.h>
9#endif
10
11#define GAIA_THREAD_OFF 0
12#define GAIA_THREAD_STD 1
13#define GAIA_THREAD_PTHREAD 2
14
15#if GAIA_PLATFORM_WINDOWS || GAIA_PLATFORM_WASM
16 #include <thread>
17 // Emscripten supports std::thread if compiled with -sUSE_PTHREADS=1.
18 // Otherwise, std::thread calls are no-ops that compile but do not run concurrently.
19 #define GAIA_THREAD std::thread
20 #define GAIA_THREAD_PLATFORM GAIA_THREAD_STD
21#elif GAIA_PLATFORM_APPLE
22 #include <pthread.h>
23 #include <pthread/sched.h>
24 #include <sys/qos.h>
25 #include <sys/sysctl.h>
26 #define GAIA_THREAD pthread_t
27 #define GAIA_THREAD_PLATFORM GAIA_THREAD_PTHREAD
28#elif GAIA_PLATFORM_LINUX
29 #include <dirent.h>
30 #include <fcntl.h>
31 #include <pthread.h>
32 #include <unistd.h>
33 #define GAIA_THREAD pthread_t
34 #define GAIA_THREAD_PLATFORM GAIA_THREAD_PTHREAD
35#elif GAIA_PLATFORM_FREEBSD
36 #include <pthread.h>
37 #include <sys/sysctl.h>
38 #define GAIA_THREAD pthread_t
39 #define GAIA_THREAD_PLATFORM GAIA_THREAD_PTHREAD
40#endif
41
42#if GAIA_PLATFORM_WINDOWS
43 #include <malloc.h>
44#else
45 #include <alloca.h>
46#endif
47#include <atomic>
48#include <thread>
49
50#include "gaia/cnt/sarray_ext.h"
51#include "gaia/core/span.h"
52#include "gaia/core/utility.h"
53#include "gaia/util/logging.h"
54
55#include "gaia/mt/event.h"
56#include "gaia/mt/futex.h"
57#include "gaia/mt/jobcommon.h"
58#include "gaia/mt/jobhandle.h"
59#include "gaia/mt/jobmanager.h"
60#include "gaia/mt/jobqueue.h"
61#include "gaia/mt/semaphore_fast.h"
62#include "gaia/mt/spinlock.h"
63
64namespace gaia {
65 namespace mt {
66#if GAIA_PLATFORM_WINDOWS
67 extern "C" typedef HRESULT(WINAPI* TOSApiFunc_SetThreadDescription)(HANDLE, PCWSTR);
68
69 #pragma pack(push, 8)
70 typedef struct tagTHREADNAME_INFO {
71 DWORD dwType; // Must be 0x1000.
72 LPCSTR szName; // Pointer to name (in user addr space).
73 DWORD dwThreadID; // Thread ID (-1=caller thread).
74 DWORD dwFlags; // Reserved for future use, must be zero.
75 } THREADNAME_INFO;
76 #pragma pack(pop)
77#endif
78
79 namespace detail {
81 inline thread_local ThreadCtx* tl_workerCtx;
82 } // namespace detail
83
84 GAIA_MSVC_WARNING_PUSH()
85 GAIA_MSVC_WARNING_DISABLE(4324)
86
88 class GAIA_API ThreadPool final {
89 friend class JobManager;
90
95 static constexpr uint32_t MaxWorkers = JobState::DEP_BITS;
96
98 std::thread::id m_mainThreadId;
99
101 std::atomic_bool m_stop{};
105 GAIA_ALIGNAS(128) cnt::sarray_ext<ThreadCtx, MaxWorkers> m_workersCtx;
107 MpmcQueue<JobHandle, 1024> m_jobQueue[JobPriorityCnt];
109 MpmcQueue<JobHandle, 1024> m_jobQueueBackground;
111 uint32_t m_frameWorkersCnt = 0;
113 uint32_t m_backgroundWorkersCnt = 0;
115 uint32_t m_workersCnt[JobPriorityCnt]{};
120 uint32_t m_workerThreadsCnt[JobPriorityCnt]{};
122 SemaphoreFast m_sem[JobPriorityCnt];
124 SemaphoreFast m_semBackground;
125
127 std::atomic_uint32_t m_blockedInWorkUntil;
128
130 JobManager m_jobManager;
136 GAIA_PROF_MUTEX(SpinLock, m_jobAllocMtx);
137
138 private:
139 ThreadPool() {
140 m_stop.store(false);
141
142 make_main_thread();
143
144 const auto hwThreads = hw_thread_cnt();
145 const auto hwEffThreads = hw_efficiency_cores_cnt();
146 uint32_t hiPrioWorkers = hwThreads;
147 if (hwEffThreads < hwThreads)
148 hiPrioWorkers -= hwEffThreads;
149
150 set_max_workers(hwThreads, hiPrioWorkers);
151 }
152
153 ThreadPool(ThreadPool&&) = delete;
154 ThreadPool(const ThreadPool&) = delete;
155 ThreadPool& operator=(ThreadPool&&) = delete;
156 ThreadPool& operator=(const ThreadPool&) = delete;
157
158 public:
161 static ThreadPool& get() {
162 static ThreadPool threadPool;
163 return threadPool;
164 }
165
166 ~ThreadPool() {
167 reset();
168 }
169
172 m_mainThreadId = std::this_thread::get_id();
173 if (!m_workersCtx.empty())
174 detail::tl_workerCtx = &m_workersCtx[0];
175 }
176
179 GAIA_NODISCARD uint32_t workers() const {
180 return m_frameWorkersCnt;
181 }
182
185 GAIA_NODISCARD uint32_t background_workers() const {
186 return m_backgroundWorkersCnt;
187 }
188
195 void set_max_workers(uint32_t count, uint32_t countHighPrio) {
196 const auto maxFrameWorkers = MaxWorkers - m_backgroundWorkersCnt;
197 const auto workersCnt = core::get_max(core::get_min(maxFrameWorkers, count), 1U);
198 countHighPrio = core::get_min(countHighPrio, workersCnt);
199
200 // Stop all threads first
201 reset();
202
203 // Reset previous worker contexts
204 for (auto& ctx: m_workersCtx)
205 ctx.reset();
206
207 m_frameWorkersCnt = workersCnt - 1;
208
209 // The main thread uses context 0. Frame workers follow, and
210 // background workers are appended after them.
211 m_workersCtx.resize(workersCnt + m_backgroundWorkersCnt);
212 // We also have the main thread so there's always one less worker spawned
213 m_workers.resize(m_frameWorkersCnt + m_backgroundWorkersCnt);
214
215 // First worker is considered the main thread.
216 // It is also assigned high priority but it doesn't really matter.
217 // The main thread can steal any jobs, both low and high priority.
218 detail::tl_workerCtx = m_workersCtx.data();
219 m_workersCtx[0].tp = this;
220 m_workersCtx[0].workerIdx = 0;
221 m_workersCtx[0].prio = JobPriority::High;
222
223 // Reset the workers
224 for (auto& worker: m_workers)
225 worker = {};
226
227 // Create a new set of high and low priority threads (if any)
228 uint32_t workerIdx = 1;
229 set_workers_high_prio_inter(workerIdx, countHighPrio);
230 create_background_worker_threads(workerIdx);
231 }
232
237 void set_workers_high_prio_inter(uint32_t& workerIdx, uint32_t count) {
238 count = gaia::core::get_min(count, m_frameWorkersCnt);
239 m_workerThreadsCnt[0] = count;
240 m_workerThreadsCnt[1] = m_frameWorkersCnt - count;
241 m_workersCnt[0] = count + 1; // Main thread is always a priority worker
242 m_workersCnt[1] = m_workerThreadsCnt[1];
243
244 // Create a new set of high and low priority threads (if any)
245 create_worker_threads(workerIdx, JobPriority::High, m_workerThreadsCnt[0]);
246 create_worker_threads(workerIdx, JobPriority::Low, m_workerThreadsCnt[1]);
247 }
248
253 void set_workers_low_prio_inter(uint32_t& workerIdx, uint32_t count) {
254 const uint32_t realCnt = gaia::core::get_min(count, m_frameWorkersCnt);
255 m_workerThreadsCnt[0] = m_frameWorkersCnt - realCnt;
256 m_workerThreadsCnt[1] = realCnt;
257 m_workersCnt[0] = m_workerThreadsCnt[0] + 1; // Main thread is always a priority worker
258 m_workersCnt[1] = m_workerThreadsCnt[1];
259
260 // Create a new set of high and low priority threads (if any)
261 create_worker_threads(workerIdx, JobPriority::High, m_workerThreadsCnt[0]);
262 create_worker_threads(workerIdx, JobPriority::Low, m_workerThreadsCnt[1]);
263 }
264
267 void set_workers_high_prio(uint32_t count) {
268 // Stop all threads first
269 reset();
270 detail::tl_workerCtx = m_workersCtx.data();
271
272 uint32_t workerIdx = 1;
273 set_workers_high_prio_inter(workerIdx, count);
274 create_background_worker_threads(workerIdx);
275 }
276
279 void set_workers_low_prio(uint32_t count) {
280 // Stop all threads first
281 reset();
282 detail::tl_workerCtx = m_workersCtx.data();
283
284 uint32_t workerIdx = 1;
285 set_workers_low_prio_inter(workerIdx, count);
286 create_background_worker_threads(workerIdx);
287 }
288
294 void set_background_workers(uint32_t count) {
295 const auto maxBackgroundWorkers = MaxWorkers - 1;
296 count = core::get_min(maxBackgroundWorkers, count);
297
298 const auto frameWorkersCntOld = m_frameWorkersCnt;
299 const auto highWorkersCntOld = m_workerThreadsCnt[0];
300
301 // Stop all threads first
302 reset();
303
304 m_backgroundWorkersCnt = count;
305
306 const auto maxFrameWorkers = MaxWorkers - m_backgroundWorkersCnt - 1;
307 m_frameWorkersCnt = core::get_min(frameWorkersCntOld, maxFrameWorkers);
308
309 for (auto& ctx: m_workersCtx)
310 ctx.reset();
311
312 m_workersCtx.resize(m_frameWorkersCnt + 1 + m_backgroundWorkersCnt);
313 m_workers.resize(m_frameWorkersCnt + m_backgroundWorkersCnt);
314
315 detail::tl_workerCtx = m_workersCtx.data();
316 m_workersCtx[0].tp = this;
317 m_workersCtx[0].workerIdx = 0;
318 m_workersCtx[0].prio = JobPriority::High;
319
320 for (auto& worker: m_workers)
321 worker = {};
322
323 uint32_t workerIdx = 1;
324 set_workers_high_prio_inter(workerIdx, highWorkersCntOld);
325 create_background_worker_threads(workerIdx);
326 }
327
333 void dep(JobHandle jobFirst, JobHandle jobSecond) {
334 GAIA_ASSERT(main_thread());
335
336 m_jobManager.dep(std::span(&jobFirst, 1), jobSecond);
337 }
338
344 void dep(std::span<JobHandle> jobsFirst, JobHandle jobSecond) {
345 GAIA_ASSERT(main_thread());
346
347 m_jobManager.dep(jobsFirst, jobSecond);
348 }
349
357 void dep_refresh(JobHandle jobFirst, JobHandle jobSecond) {
358 GAIA_ASSERT(main_thread());
359
360 m_jobManager.dep_refresh(std::span(&jobFirst, 1), jobSecond);
361 }
362
370 void dep_refresh(std::span<JobHandle> jobsFirst, JobHandle jobSecond) {
371 GAIA_ASSERT(main_thread());
372
373 m_jobManager.dep_refresh(jobsFirst, jobSecond);
374 }
375
383 template <typename TJob>
384 JobHandle add(TJob job) {
385 GAIA_ASSERT(main_thread());
386
387 job.priority = final_prio(job);
388
389 auto& mtx = GAIA_PROF_EXTRACT_MUTEX(m_jobAllocMtx);
390 core::lock_scope lock(mtx);
391 GAIA_PROF_LOCK_MARK(m_jobAllocMtx);
392
393 return m_jobManager.alloc_job(GAIA_MOV(job));
394 }
395
396 private:
397 void add_n(JobPriority prio, std::span<JobHandle> jobHandles) {
398 GAIA_ASSERT(main_thread());
399 GAIA_ASSERT(!jobHandles.empty());
400
401 auto& mtx = GAIA_PROF_EXTRACT_MUTEX(m_jobAllocMtx);
402 core::lock_scope lock(mtx);
403 GAIA_PROF_LOCK_MARK(m_jobAllocMtx);
404
405 for (auto& jobHandle: jobHandles)
406 jobHandle = m_jobManager.alloc_job({{}, prio, JobCreationFlags::Default});
407 }
408
409 GAIA_NODISCARD ParallelCallbackHandle add_parallel_callback(JobArgsFunc callback, uint32_t refs) {
410 auto& mtx = GAIA_PROF_EXTRACT_MUTEX(m_jobAllocMtx);
411 core::lock_scope lock(mtx);
412 GAIA_PROF_LOCK_MARK(m_jobAllocMtx);
413
414 return m_jobManager.alloc_parallel_callback(GAIA_MOV(callback), refs);
415 }
416
417 void release_parallel_callback(ParallelCallbackHandle handle) {
418 if (!m_jobManager.release_parallel_callback_ref(handle))
419 return;
420
421 auto& mtx = GAIA_PROF_EXTRACT_MUTEX(m_jobAllocMtx);
422 core::lock_scope lock(mtx);
423 GAIA_PROF_LOCK_MARK(m_jobAllocMtx);
424
425 m_jobManager.free_parallel_callback(handle);
426 }
427
428 void release_job(JobHandle jobHandle) {
429 auto& mtx = GAIA_PROF_EXTRACT_MUTEX(m_jobAllocMtx);
430 core::lock_scope lock(mtx);
431 GAIA_PROF_LOCK_MARK(m_jobAllocMtx);
432
433 m_jobManager.free_job(jobHandle);
434 }
435
436 public:
441 void del([[maybe_unused]] JobHandle jobHandle) {
442 GAIA_ASSERT(jobHandle != (JobHandle)JobNull_t{});
443
444 auto& mtx = GAIA_PROF_EXTRACT_MUTEX(m_jobAllocMtx);
445 core::lock_scope lock(mtx);
446 GAIA_PROF_LOCK_MARK(m_jobAllocMtx);
447 if (!m_jobManager.valid(jobHandle))
448 return;
449
450#if GAIA_ASSERT_ENABLED
451 {
452 const auto& jobData = m_jobManager.data(jobHandle);
453 GAIA_ASSERT(jobData.state == 0 || m_jobManager.done(jobData));
454 }
455#endif
456
457 m_jobManager.free_job(jobHandle);
458 }
459
466 void submit(std::span<JobHandle> jobHandles) {
467 if (jobHandles.empty())
468 return;
469
470 GAIA_PROF_SCOPE(tp::submitn);
471
472 auto* pHandles = (JobHandle*)alloca(sizeof(JobHandle) * jobHandles.size());
473
474 uint32_t cnt = 0;
475 for (auto handle: jobHandles) {
476 GAIA_ASSERT(handle != (JobHandle)JobNull_t{});
477
478 auto& jobData = m_jobManager.data(handle);
479 if GAIA_UNLIKELY (jobData.data.gen != handle.gen())
480 continue;
481
482 const auto state = m_jobManager.submit(jobData) & JobState::DEP_BITS_MASK;
483 // Jobs that were already submitted won't be submitted again.
484 // We can only accept the job if it has no pending dependencies.
485 if (state != 0)
486 continue;
487
488 pHandles[cnt++] = handle;
489 }
490
491 auto* ctx = detail::tl_workerCtx;
492 process(std::span(pHandles, cnt), ctx);
493 }
494
501 void submit(JobHandle jobHandle) {
502 GAIA_ASSERT(jobHandle != (JobHandle)JobNull_t{});
503 GAIA_PROF_SCOPE(tp::submit);
504
505 auto& jobData = m_jobManager.data(jobHandle);
506 if GAIA_UNLIKELY (jobData.data.gen != jobHandle.gen())
507 return;
508
509 const auto state = m_jobManager.submit(jobData) & JobState::DEP_BITS_MASK;
510 if (state != 0)
511 return;
512
513 auto* ctx = detail::tl_workerCtx;
514 process(std::span(&jobHandle, 1), ctx);
515 }
516
519 void reset_state(std::span<JobHandle> jobHandles) {
520 if (jobHandles.empty())
521 return;
522
523 GAIA_PROF_SCOPE(tp::reset);
524
525 for (auto handle: jobHandles) {
526 auto& jobData = m_jobManager.data(handle);
527 m_jobManager.reset_state(jobData);
528 }
529 }
530
533 void reset_state(JobHandle jobHandle) {
534 reset_state(std::span(&jobHandle, 1));
535 }
536
540 void reset(std::span<JobHandle> jobHandles) {
541 if (jobHandles.empty())
542 return;
543
544 GAIA_ASSERT(main_thread());
545 GAIA_PROF_SCOPE(tp::reset_wait);
546
547 // Wait first to avoid resetting one handle while another one still depends on it.
548 for (auto handle: jobHandles) {
549 if (handle == (JobHandle)JobNull_t{})
550 continue;
551 wait(handle);
552 }
553
554 for (auto handle: jobHandles) {
555 if (handle == (JobHandle)JobNull_t{})
556 continue;
557
558 auto& jobData = m_jobManager.data(handle);
559 const auto state = jobData.state.load() & JobState::STATE_BITS_MASK;
560 // Auto-deleted jobs are released and cannot be reused through reset_state().
561 if (state == JobState::Released)
562 continue;
563
564 m_jobManager.reset_state(jobData);
565 }
566 }
567
570 void reset(JobHandle jobHandle) {
571 reset(std::span(&jobHandle, 1));
572 }
573
580 JobHandle jobHandle = add(GAIA_MOV(job));
581 submit(jobHandle);
582 return jobHandle;
583 }
584
594 job.flags = (JobCreationFlags)((uint8_t)job.flags | (uint8_t)JobCreationFlags::Background);
595 JobHandle jobHandle = add(GAIA_MOV(job));
596 submit(jobHandle);
597 return jobHandle;
598 }
599
606 JobHandle sched(Job job, JobHandle dependsOn) {
607 JobHandle jobHandle = add(GAIA_MOV(job));
608 dep(dependsOn, jobHandle);
609 submit(jobHandle);
610 return jobHandle;
611 }
612
620 JobHandle sched_par(JobParallel job, uint32_t itemsToProcess, uint32_t groupSize) {
621 GAIA_ASSERT(main_thread());
622
623 // Empty data set are considered wrong inputs
624 GAIA_ASSERT(itemsToProcess != 0);
625 if (itemsToProcess == 0)
626 return JobNull;
627
628 // Don't add new jobs once stop was requested
629 if GAIA_UNLIKELY (m_stop)
630 return JobNull;
631
632 // Make sure the right priority is selected
633 const auto prio = job.priority = final_prio(job);
634
635 // No group size was given, make a guess based on the set size
636 if (groupSize == 0) {
637 const auto cntWorkers = core::get_max(1U, m_workersCnt[(uint32_t)prio]);
638 groupSize = itemsToProcess / cntWorkers + (itemsToProcess % cntWorkers != 0);
639
640 // If there are too many items we split them into multiple jobs.
641 // This way, if we wait for the result and some workers finish
642 // with our task faster, the finished worker can pick up a new
643 // job faster.
644 // On the other hand, too little items probably don't deserve
645 // multiple jobs.
646 constexpr uint32_t maxUnitsOfWorkPerGroup = 8;
647 groupSize = groupSize / maxUnitsOfWorkPerGroup;
648 if (groupSize <= 0)
649 groupSize = 1;
650 }
651
652 const auto jobs = itemsToProcess / groupSize + (itemsToProcess % groupSize != 0);
653
654 // Only one job is created, use the job directly.
655 // Generally, this is the case we would want to avoid because it means this particular case
656 // is not worth of being scheduled via sched_par. However, we can never know for sure what
657 // the reason for that is so let's stay silent.
658 if (jobs == 1) {
659 const uint32_t groupJobIdxEnd = groupSize < itemsToProcess ? groupSize : itemsToProcess;
660 auto groupFunc = GAIA_MOV(job.func);
661 auto groupJobFunc = [func = GAIA_MOV(groupFunc), groupJobIdxEnd]() mutable {
662 JobArgs args;
663 args.idxStart = 0;
664 args.idxEnd = groupJobIdxEnd;
665 func(args);
666 };
667
668 auto handle = add(Job{GAIA_MOV(groupJobFunc), prio, JobCreationFlags::Default});
669 submit(handle);
670 return handle;
671 }
672
673 // Multiple jobs need to be parallelized.
674 // Create a sync job and assign it as their dependency.
675 auto callbackHandle = add_parallel_callback(GAIA_MOV(job.func), jobs);
676
677 auto* pHandles = (JobHandle*)alloca(sizeof(JobHandle) * (jobs + 1));
678 std::span<JobHandle> handles(pHandles, jobs + 1);
679
680 add_n(prio, handles);
681
682#if GAIA_ASSERT_ENABLED
683 for (auto jobHandle: handles)
684 GAIA_ASSERT(m_jobManager.is_clear(jobHandle));
685#endif
686
687 // Work jobs
688 for (uint32_t jobIndex = 0; jobIndex < jobs; ++jobIndex) {
689 const uint32_t groupJobIdxStart = jobIndex * groupSize;
690 const uint32_t groupJobIdxEnd =
691 core::get_min(groupSize, itemsToProcess - groupJobIdxStart) + groupJobIdxStart;
692
693 auto groupJobFunc = [this, callbackHandle, groupJobIdxStart, groupJobIdxEnd]() {
694 JobArgs args;
695 args.idxStart = groupJobIdxStart;
696 args.idxEnd = groupJobIdxEnd;
697 m_jobManager.invoke_parallel_callback(callbackHandle, args);
698 release_parallel_callback(callbackHandle);
699 };
700
701 auto& jobData = m_jobManager.data(pHandles[jobIndex]);
702 jobData.func = util::SmallFunc::create(GAIA_MOV(groupJobFunc));
703 jobData.prio = prio;
704 }
705 // Sync job
706 {
707 auto& jobData = m_jobManager.data(pHandles[jobs]);
708 jobData.prio = prio;
709 }
710
711 // Assign the sync jobs as a dependency for work jobs
712 dep(handles.subspan(0, jobs), pHandles[jobs]);
713
714 // Sumbit the jobs to the threadpool.
715 // This is a point of no return. After this point no more changes to jobs are possible.
716 submit(handles);
717 return pHandles[jobs];
718 }
719
727 JobHandle sched_par(JobParallelRef job, uint32_t itemsToProcess, uint32_t groupSize) {
728 GAIA_ASSERT(main_thread());
729 GAIA_ASSERT(job.pCtx != nullptr);
730 GAIA_ASSERT(job.invoke != nullptr);
731
732 GAIA_ASSERT(itemsToProcess != 0);
733 if (itemsToProcess == 0)
734 return JobNull;
735
736 if GAIA_UNLIKELY (m_stop)
737 return JobNull;
738
739 const auto prio = job.priority = final_prio(job);
740
741 if (groupSize == 0) {
742 const auto cntWorkers = core::get_max(1U, m_workersCnt[(uint32_t)prio]);
743 groupSize = itemsToProcess / cntWorkers + (itemsToProcess % cntWorkers != 0);
744
745 constexpr uint32_t maxUnitsOfWorkPerGroup = 8;
746 groupSize = groupSize / maxUnitsOfWorkPerGroup;
747 if (groupSize <= 0)
748 groupSize = 1;
749 }
750
751 const auto jobs = itemsToProcess / groupSize + (itemsToProcess % groupSize != 0);
752
753 if (jobs == 1) {
754 const uint32_t groupJobIdxEnd = groupSize < itemsToProcess ? groupSize : itemsToProcess;
755 auto* pCtx = job.pCtx;
756 auto invoke = job.invoke;
757 auto groupJobFunc = [pCtx, invoke, groupJobIdxEnd]() {
758 JobArgs args;
759 args.idxStart = 0;
760 args.idxEnd = groupJobIdxEnd;
761 invoke(pCtx, args);
762 };
763
764 auto handle = add(Job{GAIA_MOV(groupJobFunc), prio, JobCreationFlags::Default});
765 submit(handle);
766 return handle;
767 }
768
769 auto* pHandles = (JobHandle*)alloca(sizeof(JobHandle) * (jobs + 1));
770 std::span<JobHandle> handles(pHandles, jobs + 1);
771
772 add_n(prio, handles);
773
774#if GAIA_ASSERT_ENABLED
775 for (auto jobHandle: handles)
776 GAIA_ASSERT(m_jobManager.is_clear(jobHandle));
777#endif
778
779 for (uint32_t jobIndex = 0; jobIndex < jobs; ++jobIndex) {
780 const uint32_t groupJobIdxStart = jobIndex * groupSize;
781 const uint32_t groupJobIdxEnd =
782 core::get_min(groupSize, itemsToProcess - groupJobIdxStart) + groupJobIdxStart;
783
784 auto* pCtx = job.pCtx;
785 auto invoke = job.invoke;
786 auto groupJobFunc = [pCtx, invoke, groupJobIdxStart, groupJobIdxEnd]() {
787 JobArgs args;
788 args.idxStart = groupJobIdxStart;
789 args.idxEnd = groupJobIdxEnd;
790 invoke(pCtx, args);
791 };
792
793 auto& jobData = m_jobManager.data(pHandles[jobIndex]);
794 jobData.func = util::SmallFunc::create(GAIA_MOV(groupJobFunc));
795 jobData.prio = prio;
796 }
797 {
798 auto& jobData = m_jobManager.data(pHandles[jobs]);
799 jobData.prio = prio;
800 }
801
802 dep(handles.subspan(0, jobs), pHandles[jobs]);
803 submit(handles);
804 return pHandles[jobs];
805 }
806
814 void wait(JobHandle jobHandle) {
815 GAIA_PROF_SCOPE(tp::wait);
816
817 GAIA_ASSERT(main_thread());
818
819 // Skip waiting for unset job handles.
820 if (jobHandle == (JobHandle)JobNull_t{})
821 return;
822
823 auto* ctx = detail::tl_workerCtx;
824 auto& jobData = m_jobManager.data(jobHandle);
825 const bool waitBackground = is_background(jobData);
826 auto state = jobData.state.load(std::memory_order_acquire);
827
828 // Waiting for a job that has not been initialized is nonsense.
829 GAIA_ASSERT(state != 0);
830
831 // Wait until done
832 for (; (state & JobState::STATE_BITS_MASK) < JobState::Done;
833 state = jobData.state.load(std::memory_order_acquire)) {
834 // The job we are waiting for is not finished yet, try running some other job in the meantime
835 JobHandle otherJobHandle;
836 const bool canHelpBackground = waitBackground && m_backgroundWorkersCnt == 0;
837 const bool hasBackgroundJob = canHelpBackground && try_fetch_background_job(otherJobHandle);
838 const bool hasJob = hasBackgroundJob || try_fetch_job(*ctx, otherJobHandle);
839 if (hasJob) {
840 if (run(otherJobHandle, ctx))
841 continue;
842 }
843
844 // The job we are waiting for is already running.
845 // Wait until it signals it's finished.
846 if ((state & JobState::STATE_BITS_MASK) == JobState::Executing) {
847 const auto workerId = (state & JobState::DEP_BITS_MASK);
848 auto* jobDoneEvent = &m_workersCtx[workerId].event;
849 jobDoneEvent->wait();
850 continue;
851 }
852
853 // The worst case scenario.
854 // We have nothing to do and the job we are waiting for is not executing still.
855 // Let's wait for any job to start executing.
856 const auto workerBit = 1U << ctx->workerIdx;
857 const auto oldBlockedMask = m_blockedInWorkUntil.fetch_or(workerBit);
858 const auto newState = jobData.state.load();
859 if (newState == state) // still not JobState::Done?
860 Futex::wait(&m_blockedInWorkUntil, oldBlockedMask | workerBit, detail::WaitMaskAny);
861 m_blockedInWorkUntil.fetch_and(~workerBit);
862 }
863 }
864
867 void update() {
868 GAIA_ASSERT(main_thread());
869 main_thread_tick();
870 }
871
874 GAIA_NODISCARD static uint32_t hw_thread_cnt() {
875 auto hwThreads = (uint32_t)std::thread::hardware_concurrency();
876 return core::get_max(1U, hwThreads);
877 }
878
881 GAIA_NODISCARD static uint32_t hw_efficiency_cores_cnt() {
882 uint32_t efficiencyCores = 0;
883#if GAIA_PLATFORM_APPLE
884 size_t size = sizeof(efficiencyCores);
885 if (sysctlbyname("hw.perflevel1.logicalcpu", &efficiencyCores, &size, nullptr, 0) != 0)
886 return 0;
887#elif GAIA_PLATFORM_FREEBSD
888 int cpuIndex = 0;
889 char oidName[32];
890 int coreType;
891 size_t size = sizeof(coreType);
892 while (true) {
893 GAIA_STRFMT(oidName, sizeof(oidName), "dev.cpu.%d.coretype", cpuIndex);
894 if (sysctlbyname(oidName, &coreType, &size, nullptr, 0) != 0)
895 break; // Stop on the last CPU index
896
897 // 0 = performance core
898 // 1 = efficiency core
899 if (coreType == 1)
900 ++efficiencyCores;
901
902 ++cpuIndex;
903 }
904#elif GAIA_PLATFORM_WINDOWS
905 DWORD length = 0;
906
907 // First, determine required buffer size
908 if (!GetLogicalProcessorInformationEx(RelationProcessorCore, nullptr, &length))
909 return 0;
910
911 // Allocate enough memory
912 auto* pBuffer = (SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX*)malloc(length);
913 if (pBuffer == nullptr)
914 return 0;
915
916 // Retrieve the data
917 if (!GetLogicalProcessorInformationEx(RelationProcessorCore, pBuffer, &length)) {
918 free(pBuffer);
919 return 0;
920 }
921
922 uint32_t heterogenousCnt = 0;
923
924 // Iterate over processor core entries.
925 // On Windows we can't directly tell whether a core is an efficiency core or a performance core.
926 // Instead:
927 // - lower EfficiencyClass values correspond to more efficient cores
928 // - higher EfficiencyClass values correspond to higher performance cores
929 // - EfficiencyClass is zero for homogeneous CPU architectures
930 // Therefore, to count efficiency cores on Windows, we will count cores where EfficiencyClass == 0.
931 // On heterogeneous this should gives us the correct results.
932 // On homogenous architectures, the value is always 0 so rather than calculating the number of efficiency
933 // cores, we would calculate the number of performance cores. For the sake of correctness, if all cores return
934 // 0, we use 0 for the number of efficiency cores.
935 for (char* ptr = (char*)pBuffer; ptr < (char*)pBuffer + length;
936 ptr += ((SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX*)ptr)->Size) {
937 auto* entry = (SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX*)ptr;
938 if (entry->Relationship == RelationProcessorCore) {
939 if (entry->Processor.EfficiencyClass == 0)
940 ++efficiencyCores;
941 else
942 ++heterogenousCnt;
943 }
944 }
945
946 if (heterogenousCnt == 0)
947 efficiencyCores = 0;
948
949 free(pBuffer);
950#elif GAIA_PLATFORM_LINUX
951 {
952 // Intel has /sys/devices/cpu_core/cpus, /sys/devices/cpu_atom/cpus on some systems
953 DIR* dir = opendir("/sys/devices/cpu_atom/cpus/");
954 if (dir == nullptr)
955 return 0;
956
957 dirent* entry;
958 while ((entry = readdir(dir)) != nullptr) {
959 if (strncmp(entry->d_name, "cpu", 3) == 0 && entry->d_name[3] >= '0' && entry->d_name[3] <= '9')
960 ++efficiencyCores;
961 }
962
963 closedir(dir);
964 }
965
966 if (efficiencyCores == 0) {
967 // TODO: Go through all CPUs packages and CPUs and determine the differences between them.
968 // There are many metrics.
969 // 1) We will assume all CPUs to be of the same architecture.
970 // 2) Same CPU architecture but different cache sizes. Smaller ones are "efficiency" cores.
971 // This is the AMD way. Still, these are about the same things so maybe we would just treat
972 // all such cores as performance cores.
973 // 3) Different max frequencies on different cores. This might be indicative enough.
974 // There is also an optional parameter present on ARM CPUs:
975 // https://www.kernel.org/doc/Documentation/devicetree/bindings/arm/cpu-capacity.txt
976 // In this case, we'd treat CPUs with the highest capacity-dmips-mhz as performance cores,
977 // and consider the rest as efficiency cores.
978
979 // ...
980 }
981#endif
982 return efficiencyCores;
983 }
984
985 private:
986 static void* thread_func(void* pCtx) {
987 auto& ctx = *(ThreadCtx*)pCtx;
988
989 detail::tl_workerCtx = &ctx;
990
991 // Set the worker thread name.
992 // Needs to be called from inside the thread because some platforms
993 // can change the name only when run from the specific thread.
994 ctx.tp->set_thread_name(ctx.workerIdx, ctx.prio);
995
996 // Set the worker thread priority
997 ctx.tp->set_thread_priority(ctx.workerIdx, ctx.prio);
998
999 // Process jobs
1000 ctx.tp->worker_loop(ctx);
1001
1002 detail::tl_workerCtx = nullptr;
1003
1004 return nullptr;
1005 }
1006
1011 void create_thread(uint32_t workerIdx, JobPriority prio, bool background) {
1012 // Idx 0 is reserved for the main thread
1013 GAIA_ASSERT(workerIdx > 0);
1014
1015 auto& ctx = m_workersCtx[workerIdx];
1016 ctx.tp = this;
1017 ctx.workerIdx = workerIdx;
1018 ctx.prio = prio;
1019 ctx.background = background;
1020 ctx.threadCreated = false;
1021
1022#if GAIA_THREAD_PLATFORM == GAIA_THREAD_STD
1023 m_workers[workerIdx - 1] = std::thread([&ctx]() {
1024 thread_func((void*)&ctx);
1025 });
1026#else
1027 pthread_attr_t attr{};
1028 int ret = pthread_attr_init(&attr);
1029 if (ret != 0) {
1030 GAIA_LOG_W("pthread_attr_init failed for worker thread %u. ErrCode = %d", workerIdx, ret);
1031 return;
1032 }
1033
1035 // Apple's recommendation for Apple Silicon for games / high-perf software
1036 // ========================================================================
1037 // Per frame | Scheduling policy | QoS class / Priority
1038 // ========================================================================
1039 // Main thread | SCHED_OTHER | QOS_CLASS_USER_INTERACTIVE (47)
1040 // Render/Audio thread | SCHED_RR | 45
1041 // Workers High Prio | SCHED_RR | 39-41
1042 // Workers Low Prio | SCHED_OTHER | QOS_CLASS_USER_INTERACTIVE (38)
1043 // ========================================================================
1044 // Multiple-frames | |
1045 // ========================================================================
1046 // Async Workers High Prio| SCHED_OTHER | QOS_CLASS_USER_INITIATED (37)
1047 // Async Workers Low Prio | SCHED_OTHER | QOS_CLASS_DEFAULT (31)
1048 // Prefetching/Streaming | SCHED_OTHER | QOS_CLASS_UTILITY (20)
1049 // ========================================================================
1050
1051 if (prio == JobPriority::Low) {
1052 #if GAIA_PLATFORM_APPLE
1053 ret = pthread_attr_set_qos_class_np(&attr, QOS_CLASS_USER_INTERACTIVE, -9); // 47-9=38
1054 if (ret != 0) {
1055 GAIA_LOG_W(
1056 "pthread_attr_set_qos_class_np failed for worker thread %u [prio=%u]. ErrCode = %d", workerIdx,
1057 (uint32_t)prio, ret);
1058 }
1059 #else
1060 ret = pthread_attr_setschedpolicy(&attr, SCHED_OTHER);
1061 if (ret != 0) {
1062 GAIA_LOG_W(
1063 "pthread_attr_setschedpolicy SCHED_RR failed for worker thread %u [prio=%u]. ErrCode = %d", workerIdx,
1064 (uint32_t)prio, ret);
1065 }
1066
1067 int prioMax = core::get_min(38, sched_get_priority_max(SCHED_OTHER));
1068 int prioMin = core::get_min(prioMax, sched_get_priority_min(SCHED_OTHER));
1069 int prioUse = core::get_min(prioMin + 5, prioMax);
1070 prioUse = core::get_max(prioUse, prioMin);
1071 sched_param param{};
1072 param.sched_priority = prioUse;
1073
1074 ret = pthread_attr_setschedparam(&attr, &param);
1075 if (ret != 0) {
1076 GAIA_LOG_W(
1077 "pthread_attr_setschedparam %d failed for worker thread %u [prio=%u]. ErrCode = %d",
1078 param.sched_priority, workerIdx, (uint32_t)prio, ret);
1079 }
1080 #endif
1081 } else {
1082 ret = pthread_attr_setschedpolicy(&attr, SCHED_RR);
1083 if (ret != 0) {
1084 GAIA_LOG_W(
1085 "pthread_attr_setschedpolicy SCHED_RR failed for worker thread %u [prio=%u]. ErrCode = %d", workerIdx,
1086 (uint32_t)prio, ret);
1087 }
1088
1089 int prioMax = core::get_min(41, sched_get_priority_max(SCHED_RR));
1090 int prioMin = core::get_min(prioMax, sched_get_priority_min(SCHED_RR));
1091 int prioUse = core::get_max(prioMax - 5, prioMin);
1092 prioUse = core::get_min(prioUse, prioMax);
1093 sched_param param{};
1094 param.sched_priority = prioUse;
1095
1096 ret = pthread_attr_setschedparam(&attr, &param);
1097 if (ret != 0) {
1098 GAIA_LOG_W(
1099 "pthread_attr_setschedparam %d failed for worker thread %u [prio=%u]. ErrCode = %d",
1100 param.sched_priority, workerIdx, (uint32_t)prio, ret);
1101 }
1102 }
1103
1104 // Create the thread with given attributes
1105 ret = pthread_create(&m_workers[workerIdx - 1], &attr, thread_func, (void*)&ctx);
1106 if (ret != 0) {
1107 GAIA_LOG_W("pthread_create failed for worker thread %u. ErrCode = %d", workerIdx, ret);
1108 } else {
1109 ctx.threadCreated = true;
1110 }
1111
1112 pthread_attr_destroy(&attr);
1113#endif
1114
1115 // Stick each thread to a specific CPU core if possible
1116 set_thread_affinity(workerIdx);
1117 }
1118
1121 void join_thread(uint32_t workerIdx) {
1122 if GAIA_UNLIKELY (workerIdx > m_workers.size())
1123 return;
1124
1125#if GAIA_THREAD_PLATFORM == GAIA_THREAD_STD
1126 auto& t = m_workers[workerIdx - 1];
1127 if (t.joinable())
1128 t.join();
1129#else
1130 auto& ctx = m_workersCtx[workerIdx];
1131 if (!ctx.threadCreated)
1132 return;
1133
1134 auto& t = m_workers[workerIdx - 1];
1135 pthread_join(t, nullptr);
1136 ctx.threadCreated = false;
1137#endif
1138 }
1139
1140 void create_worker_threads(uint32_t& workerIdx, JobPriority prio, uint32_t count) {
1141 for (uint32_t i = 0; i < count; ++i)
1142 create_thread(workerIdx++, prio, false);
1143 }
1144
1145 void create_background_worker_threads(uint32_t& workerIdx) {
1146 for (uint32_t i = 0; i < m_backgroundWorkersCnt; ++i)
1147 create_thread(workerIdx++, JobPriority::Low, true);
1148 }
1149
1150 void set_thread_priority([[maybe_unused]] uint32_t workerIdx, [[maybe_unused]] JobPriority priority) {
1151#if GAIA_PLATFORM_WINDOWS
1152 HANDLE nativeHandle = (HANDLE)m_workers[workerIdx - 1].native_handle();
1153
1154 THREAD_POWER_THROTTLING_STATE state{};
1155 state.Version = THREAD_POWER_THROTTLING_CURRENT_VERSION;
1156 if (priority == JobPriority::High) {
1157 // HighQoS
1158 // Turn EXECUTION_SPEED throttling off.
1159 // ControlMask selects the mechanism and StateMask is set to zero as mechanisms should be turned off.
1160 state.ControlMask = THREAD_POWER_THROTTLING_EXECUTION_SPEED;
1161 state.StateMask = 0;
1162 } else {
1163 // EcoQoS
1164 // Turn EXECUTION_SPEED throttling on.
1165 // ControlMask selects the mechanism and StateMask declares which mechanism should be on or off.
1166 state.ControlMask = THREAD_POWER_THROTTLING_EXECUTION_SPEED;
1167 state.StateMask = THREAD_POWER_THROTTLING_EXECUTION_SPEED;
1168 }
1169
1170 BOOL ret = SetThreadInformation(nativeHandle, ThreadPowerThrottling, &state, sizeof(state));
1171 if (ret != TRUE) {
1172 GAIA_LOG_W("SetThreadInformation failed for thread %u", workerIdx);
1173 return;
1174 }
1175#else
1176 // Done when the thread is created
1177#endif
1178 }
1179
1180 void set_thread_affinity([[maybe_unused]] uint32_t workerIdx) {
1181 // NOTE:
1182 // Some cores might have multiple logic threads, there might be
1183 // more sockets and some cores might even be physically different
1184 // form others (performance vs efficiency cores).
1185 // Because of that, do not handle affinity and let the OS figure it out.
1186 // All treads created by the pool are setting thread priorities to make
1187 // it easier for the OS.
1188
1189 // #if GAIA_PLATFORM_WINDOWS
1190 // HANDLE nativeHandle = (HANDLE)m_workers[workerIdx-1].native_handle();
1191 //
1192 // auto mask = SetThreadAffinityMask(nativeHandle, 1ULL << workerIdx);
1193 // if (mask <= 0)
1194 // GAIA_LOG_W("Issue setting thread affinity for worker thread %u!", workerIdx);
1195 // #elif GAIA_PLATFORM_APPLE
1196 // // Do not do affinity for MacOS. If is not supported for Apple Silicon and
1197 // // Intel MACs are deprecated anyway.
1198 // // TODO: Consider supporting this at least for Intel MAC as there are still
1199 // // quite of few of them out there.
1200 // #elif GAIA_PLATFORM_LINUX || GAIA_PLATFORM_FREEBSD
1201 // pthread_t nativeHandle = (pthread_t)m_workers[workerIdx-1].native_handle();
1202 //
1203 // cpu_set_t cpuSet;
1204 // CPU_ZERO(&cpuSet);
1205 // CPU_SET(workerIdx, &cpuSet);
1206 //
1207 // auto ret = pthread_setaffinity_np(nativeHandle, sizeof(cpuSet), &cpuSet);
1208 // if (ret != 0)
1209 // GAIA_LOG_W("Issue setting thread affinity for worker thread %u!", workerIdx);
1210 //
1211 // ret = pthread_getaffinity_np(nativeHandle, sizeof(cpuSet), &cpuSet);
1212 // if (ret != 0)
1213 // GAIA_LOG_W("Thread affinity could not be set for worker thread %u!", workerIdx);
1214 // #endif
1215 }
1216
1220 void set_thread_name(uint32_t workerIdx, JobPriority prio) {
1221 const bool background = m_workersCtx[workerIdx].background;
1222 const char* workerKind = background ? "BG" : prio == JobPriority::High ? "HI" : "LO";
1223#if GAIA_PROF_USE_PROFILER_THREAD_NAME
1224 char threadName[16]{};
1225 GAIA_STRFMT(threadName, 16, "worker_%s_%u", workerKind, workerIdx);
1226 GAIA_PROF_THREAD_NAME(threadName);
1227#elif GAIA_PLATFORM_WINDOWS
1228 auto nativeHandle = (HANDLE)m_workers[workerIdx - 1].native_handle();
1229 const wchar_t* workerKindW = background ? L"BG" : prio == JobPriority::High ? L"HI" : L"LO";
1230
1231 TOSApiFunc_SetThreadDescription pSetThreadDescFunc = nullptr;
1232 if (auto* pModule = GetModuleHandleA("kernel32.dll")) {
1233 auto* pFunc = GetProcAddress(pModule, "SetThreadDescription");
1234 pSetThreadDescFunc = reinterpret_cast<TOSApiFunc_SetThreadDescription>(reinterpret_cast<void*>(pFunc));
1235 }
1236 if (pSetThreadDescFunc != nullptr) {
1237 wchar_t threadName[16]{};
1238 swprintf_s(threadName, L"worker_%s_%u", workerKindW, workerIdx);
1239
1240 auto hr = pSetThreadDescFunc(nativeHandle, threadName);
1241 if (FAILED(hr)) {
1242 GAIA_LOG_W("Issue setting name for worker %s thread %u!", workerKind, workerIdx);
1243 }
1244 } else {
1245 #if defined _MSC_VER
1246 char threadName[16]{};
1247 GAIA_STRFMT(threadName, 16, "worker_%s_%u", workerKind, workerIdx);
1248
1249 THREADNAME_INFO info{};
1250 info.dwType = 0x1000;
1251 info.szName = threadName;
1252 info.dwThreadID = GetThreadId(nativeHandle);
1253
1254 __try {
1255 RaiseException(0x406D1388, 0, sizeof(info) / sizeof(ULONG_PTR), (ULONG_PTR*)&info);
1256 } __except (EXCEPTION_EXECUTE_HANDLER) {
1257 }
1258 #endif
1259 }
1260#elif GAIA_PLATFORM_APPLE
1261 char threadName[16]{};
1262 GAIA_STRFMT(threadName, 16, "worker_%s_%u", workerKind, workerIdx);
1263 auto ret = pthread_setname_np(threadName);
1264 if (ret != 0)
1265 GAIA_LOG_W("Issue setting name for worker %s thread %u!", workerKind, workerIdx);
1266#elif GAIA_PLATFORM_LINUX || GAIA_PLATFORM_FREEBSD
1267 auto nativeHandle = m_workers[workerIdx - 1];
1268
1269 char threadName[16]{};
1270 GAIA_STRFMT(threadName, 16, "worker_%s_%u", workerKind, workerIdx);
1271 GAIA_PROF_THREAD_NAME(threadName);
1272 auto ret = pthread_setname_np(nativeHandle, threadName);
1273 if (ret != 0)
1274 GAIA_LOG_W("Issue setting name for worker %s thread %u!", workerKind, workerIdx);
1275#endif
1276 }
1277
1280 GAIA_NODISCARD bool main_thread() const {
1281 return std::this_thread::get_id() == m_mainThreadId;
1282 }
1283
1286 void main_thread_tick() {
1287 auto& ctx = *detail::tl_workerCtx;
1288
1289 // Keep executing while there is work
1290 while (true) {
1291 JobHandle jobHandle;
1292 if (!try_fetch_job(ctx, jobHandle))
1293 break;
1294
1295 (void)run(jobHandle, &ctx);
1296 }
1297 }
1298
1304 GAIA_NODISCARD bool try_steal_job(ThreadCtx& ctx, JobPriority prio, JobHandle& jobHandle) {
1305 const auto workerCnt = m_workersCtx.size();
1306 for (uint32_t i = 0; i < workerCnt;) {
1307 // Keep stealing within the same priority class and skip our own queue
1308 if (i == ctx.workerIdx || m_workersCtx[i].background || m_workersCtx[i].prio != prio) {
1309 ++i;
1310 continue;
1311 }
1312
1313 const auto res = m_workersCtx[i].jobQueue.try_steal(jobHandle);
1314 // Race condition, try again from the same context
1315 if (!res)
1316 continue;
1317
1318 // Stealing can return true if the queue is empty.
1319 // We return right away only if we receive a valid handle which means
1320 // when there was an idle job in the queue.
1321 if (jobHandle != (JobHandle)JobNull_t{})
1322 return true;
1323
1324 ++i;
1325 }
1326
1327 return false;
1328 }
1329
1335 GAIA_NODISCARD bool try_fetch_prio(ThreadCtx& ctx, JobPriority prio, JobHandle& jobHandle) {
1336 if (m_jobQueue[(uint32_t)prio].try_pop(jobHandle))
1337 return true;
1338
1339 return try_steal_job(ctx, prio, jobHandle);
1340 }
1341
1345 GAIA_NODISCARD bool try_fetch_background_job(JobHandle& jobHandle) {
1346 return m_jobQueueBackground.try_pop(jobHandle);
1347 }
1348
1353 GAIA_NODISCARD bool try_fetch_job(ThreadCtx& ctx, JobHandle& jobHandle) {
1354 if (ctx.background)
1355 return try_fetch_background_job(jobHandle);
1356
1357 // Try getting a job from the local queue
1358 if (ctx.jobQueue.try_pop(jobHandle))
1359 return true;
1360
1361 // The main thread may help with both queues while waiting or updating
1362 if (ctx.workerIdx == 0) {
1363 if (try_fetch_prio(ctx, JobPriority::High, jobHandle))
1364 return true;
1365
1366 return try_fetch_prio(ctx, JobPriority::Low, jobHandle);
1367 }
1368
1369 return try_fetch_prio(ctx, ctx.prio, jobHandle);
1370 }
1371
1376 GAIA_NODISCARD bool can_run_inline(const ThreadCtx* ctx, const JobContainer& jobData) const {
1377 const bool background = is_background(jobData);
1378 if (background)
1379 return (ctx != nullptr && ctx->background) || m_backgroundWorkersCnt == 0;
1380
1381 // The main thread is allowed to help with both priority classes.
1382 if (ctx == nullptr || ctx->workerIdx == 0)
1383 return true;
1384
1385 // Matching worker classes may execute their own overflow inline.
1386 if (!ctx->background && ctx->prio == jobData.prio)
1387 return true;
1388
1389 // If there are no spawned workers for the target priority we need to preserve
1390 // the forward-progress guarantee and allow inline fallback.
1391 return m_workerThreadsCnt[(uint32_t)jobData.prio] == 0;
1392 }
1393
1397 void wait_for_queue_space(ThreadCtx& ctx, const JobContainer& jobData) {
1398 const bool background = is_background(jobData);
1399
1400 // Wake one worker from the target class in case all of them are asleep while
1401 // the producer is waiting for queue space to become available.
1402 if (background) {
1403 if (m_backgroundWorkersCnt != 0)
1404 m_semBackground.release(1);
1405 } else {
1406 const auto prioIdx = (uint32_t)jobData.prio;
1407 if (m_workerThreadsCnt[prioIdx] != 0)
1408 m_sem[prioIdx].release(1);
1409 }
1410
1411 // Keep the current worker productive without violating the priority boundary.
1412 JobHandle otherJobHandle;
1413 const bool hasWork =
1414 ctx.background ? try_fetch_background_job(otherJobHandle) : try_fetch_prio(ctx, ctx.prio, otherJobHandle);
1415 if (hasWork) {
1416 (void)run(otherJobHandle, &ctx);
1417 return;
1418 }
1419
1420 std::this_thread::yield();
1421 }
1422
1426 void worker_loop(ThreadCtx& ctx) {
1427 while (true) {
1428 // Wait for work
1429 if (ctx.background)
1430 m_semBackground.wait();
1431 else
1432 m_sem[(uint32_t)ctx.prio].wait();
1433
1434 // Keep executing while there is work
1435 while (true) {
1436 JobHandle jobHandle;
1437 if (!try_fetch_job(ctx, jobHandle))
1438 break;
1439
1440 (void)run(jobHandle, detail::tl_workerCtx);
1441 }
1442
1443 // Check if the worker can keep running
1444 const bool stop = m_stop.load();
1445 if (stop)
1446 break;
1447 }
1448 }
1449
1451 void reset() {
1452 if (m_workers.empty())
1453 return;
1454
1455 // Request stopping
1456 m_stop.store(true);
1457
1458 // Signal all threads
1459 GAIA_FOR(JobPriorityCnt) {
1460 if (m_workerThreadsCnt[i] != 0)
1461 m_sem[i].release((int32_t)m_workerThreadsCnt[i]);
1462 }
1463 if (m_backgroundWorkersCnt != 0)
1464 m_semBackground.release((int32_t)m_backgroundWorkersCnt);
1465
1466 auto* ctx = detail::tl_workerCtx;
1467 if (ctx == nullptr) {
1468 // reset() can be reached during static teardown from a thread that never
1469 // entered the pool and therefore has no TLS worker context bound.
1470 ctx = &m_workersCtx[0];
1471 detail::tl_workerCtx = ctx;
1472 }
1473
1474 // Finish remaining jobs
1475 JobHandle jobHandle;
1476 while (try_fetch_job(*ctx, jobHandle)) {
1477 run(jobHandle, ctx);
1478 }
1479 while (try_fetch_background_job(jobHandle)) {
1480 run(jobHandle, ctx);
1481 }
1482
1483 detail::tl_workerCtx = nullptr;
1484
1485 // Join threads with the main one
1486 GAIA_FOR(m_workers.size()) join_thread(i + 1);
1487
1488 // All threads have been stopped. Allow new threads to run if necessary.
1489 m_stop.store(false);
1490 }
1491
1493 JobPriority final_frame_prio(JobPriority priority) {
1494 const auto cntWorkers = m_workersCnt[(uint32_t)priority];
1495 return cntWorkers > 0
1496 // If there is enough workers, keep the priority
1497 ? priority
1498 // Not enough workers, use the other priority that has workers
1499 : (JobPriority)(((uint32_t)priority + 1U) % (uint32_t)JobPriorityCnt);
1500 }
1501
1503 JobPriority final_prio(const Job& job) {
1504 if ((job.flags & JobCreationFlags::Background) != 0U)
1505 return job.priority;
1506
1507 return final_frame_prio(job.priority);
1508 }
1509
1511 template <typename TJob>
1512 JobPriority final_prio(const TJob& job) {
1513 return final_frame_prio(job.priority);
1514 }
1515
1519 GAIA_NODISCARD static bool is_background(const JobContainer& jobData) {
1520 return (jobData.flags & JobCreationFlags::Background) != 0U;
1521 }
1522
1523 uint32_t signal_edges(JobContainer& jobData, JobHandle* pReadyHandles) {
1524 const auto max = jobData.edges.depCnt;
1525
1526 // Nothing to do if there are no dependencies
1527 if (max == 0)
1528 return 0;
1529
1530 // One dependency
1531 if (max == 1) {
1532 auto depHandle = jobData.edges.dep;
1533#if GAIA_LOG_JOB_STATES
1534 GAIA_LOG_N("SIGNAL %u.%u -> %u.%u", jobData.idx, jobData.gen, depHandle.id(), depHandle.gen());
1535#endif
1536
1537 // See the conditions can't be satisfied for us to submit the job we skip
1538 auto& depData = m_jobManager.data(depHandle);
1539 if (!JobManager::signal_edge(depData))
1540 return 0;
1541
1542 pReadyHandles[0] = depHandle;
1543 return 1;
1544 }
1545
1546 // Multiple dependencies. The array has to be set
1547 GAIA_ASSERT(jobData.edges.pDeps != nullptr);
1548
1549 uint32_t cnt = 0;
1550 GAIA_FOR(max) {
1551 auto depHandle = jobData.edges.pDeps[i];
1552
1553 // See if all conditions were satisfied for us to submit the job
1554 auto& depData = m_jobManager.data(depHandle);
1555 if (!JobManager::signal_edge(depData))
1556 continue;
1557
1558 pReadyHandles[cnt++] = depHandle;
1559 }
1560
1561 return cnt;
1562 }
1563
1567 void process(std::span<JobHandle> jobHandles, ThreadCtx* ctx) {
1568 auto* pHandles = (JobHandle*)alloca(sizeof(JobHandle) * jobHandles.size());
1569 uint32_t handlesCnt = 0;
1570
1571 for (auto handle: jobHandles) {
1572 auto& jobData = m_jobManager.data(handle);
1573 m_jobManager.processing(jobData);
1574
1575 // Jobs that have no functor assigned don't need to be enqueued.
1576 // We can "run" them right away. The only time where it makes
1577 // sense to create such a job is to create a sync job. E.g. when you
1578 // need to wait for N jobs, rather than waiting for each of them
1579 // separately you make them a dependency of a dummy/sync job and
1580 // wait just for that one.
1581 if (!jobData.func.operator bool())
1582 (void)run(handle, ctx);
1583 else
1584 pHandles[handlesCnt++] = handle;
1585 }
1586
1587 std::span handles(pHandles, handlesCnt);
1588 while (!handles.empty()) {
1589 // Try pushing all jobs while preserving their priority queue ownership.
1590 uint32_t pushed = 0;
1591 uint32_t released[JobPriorityCnt]{};
1592 uint32_t backgroundReleased = 0;
1593 for (; pushed < handles.size(); ++pushed) {
1594 const auto handle = handles[pushed];
1595 const auto& jobData = m_jobManager.data(handle);
1596 if (is_background(jobData)) {
1597 if (!m_jobQueueBackground.try_push(handle))
1598 break;
1599
1600 ++backgroundReleased;
1601 continue;
1602 }
1603
1604 const auto prio = jobData.prio;
1605 // Worker-local queues are reserved for work that matches the worker's own
1606 // priority class. Cross-priority releases must go through the matching
1607 // global queue so the right workers can pick them up.
1608 const bool useLocalQueue = ctx != nullptr && !ctx->background && ctx->workerIdx != 0 && ctx->prio == prio;
1609 const bool res =
1610 useLocalQueue ? ctx->jobQueue.try_push(handle) : m_jobQueue[(uint32_t)prio].try_push(handle);
1611 if (!res)
1612 break;
1613
1614 released[(uint32_t)prio]++;
1615 }
1616
1617 GAIA_FOR(JobPriorityCnt) {
1618 // Only spawned worker threads block on semaphores. The main thread helps by
1619 // draining queues opportunistically from wait() and update().
1620 const auto cnt = core::get_min(released[i], m_workerThreadsCnt[i]);
1621 if (cnt != 0)
1622 m_sem[i].release((int32_t)cnt);
1623 }
1624 const auto backgroundCnt = core::get_min(backgroundReleased, m_backgroundWorkersCnt);
1625 if (backgroundCnt != 0)
1626 m_semBackground.release((int32_t)backgroundCnt);
1627
1628 handles = handles.subspan(pushed);
1629 if (!handles.empty()) {
1630 const auto handle = handles[0];
1631 const auto& jobData = m_jobManager.data(handle);
1632
1633 if (can_run_inline(ctx, jobData)) {
1634 // The queue was full. Execute the job right away only when the
1635 // current execution context is allowed to run this priority class.
1636 run(handle, ctx);
1637 handles = handles.subspan(1);
1638 } else {
1639 GAIA_ASSERT(ctx != nullptr);
1640 wait_for_queue_space(*ctx, jobData);
1641 }
1642 }
1643 }
1644 }
1645
1646 bool run(JobHandle jobHandle, ThreadCtx* ctx) {
1647 if (jobHandle == (JobHandle)JobNull_t{})
1648 return false;
1649
1650 auto& jobData = m_jobManager.data(jobHandle);
1651 const bool manualDelete = (jobData.flags & JobCreationFlags::ManualDelete) != 0U;
1652 const bool canWait = (jobData.flags & JobCreationFlags::CanWait) != 0U;
1653
1654 m_jobManager.executing(jobData, ctx->workerIdx);
1655
1656 if (m_blockedInWorkUntil.load() != 0) {
1657 const auto blockedCnt = m_blockedInWorkUntil.exchange(0);
1658 if (blockedCnt != 0)
1659 Futex::wake(&m_blockedInWorkUntil, detail::WaitMaskAll);
1660 }
1661
1662 GAIA_ASSERT(jobData.idx != (uint32_t)-1 && jobData.data.gen != (uint32_t)-1);
1663
1664 // Run the functor associated with the job
1665 m_jobManager.run(jobData);
1666
1667 if (jobData.edges.depCnt == 0) {
1668 JobManager::finalize(jobData);
1669 } else {
1670 // Resolve outgoing edges before publishing completion so the job can be reused safely.
1671 auto* pReadyHandles = (JobHandle*)alloca(sizeof(JobHandle) * jobData.edges.depCnt);
1672 const auto readyHandlesCnt = signal_edges(jobData, pReadyHandles);
1673 if (!manualDelete)
1674 JobManager::free_edges(jobData);
1675 JobManager::finalize(jobData);
1676
1677 // Dependents can only start after the prerequisite has published its final state.
1678 process(std::span(pReadyHandles, readyHandlesCnt), ctx);
1679 }
1680
1681 // Signal we finished
1682 ctx->event.set();
1683 if (canWait) {
1684 const auto* pFutexValue = &jobData.state;
1685 Futex::wake(pFutexValue, detail::WaitMaskAll);
1686 }
1687
1688 if (!manualDelete)
1689 release_job(jobHandle);
1690
1691 return true;
1692 }
1693 };
1694
1695 GAIA_MSVC_WARNING_POP()
1696 } // namespace mt
1697} // namespace gaia
Array with variable size of elements of type.
Definition darray_impl.h:27
GAIA_NODISCARD size_type size() const noexcept
Returns the number of elements.
Definition darray_impl.h:504
void resize(size_type count)
Changes the number of elements.
Definition darray_impl.h:240
GAIA_NODISCARD bool empty() const noexcept
Checks whether the container has no elements.
Definition darray_impl.h:510
GAIA_NODISCARD pointer data() noexcept
Returns a pointer to the element storage.
Definition darray_impl.h:193
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:88
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:237
JobHandle add(TJob job)
Creates a threadpool job from job.
Definition threadpool.h:384
GAIA_NODISCARD uint32_t background_workers() const
Returns the number of background worker threads.
Definition threadpool.h:185
JobHandle sched_par(JobParallelRef job, uint32_t itemsToProcess, uint32_t groupSize)
Schedules a non-owning parallel job descriptor on worker threads.
Definition threadpool.h:727
void update()
Uses the main thread to help with frame job processing. Background jobs are intentionally excluded fr...
Definition threadpool.h:867
void reset(std::span< JobHandle > jobHandles)
Waits for jobHandles to finish and resets them to a reusable state.
Definition threadpool.h:540
JobHandle sched_par(JobParallel job, uint32_t itemsToProcess, uint32_t groupSize)
Schedules a job to run on worker threads in parallel.
Definition threadpool.h:620
void dep(JobHandle jobFirst, JobHandle jobSecond)
Makes jobSecond depend on jobFirst. This means jobSecond will not run until jobFirst finishes.
Definition threadpool.h:333
void set_max_workers(uint32_t count, uint32_t countHighPrio)
Set the maximum number of frame execution contexts for this system.
Definition threadpool.h:195
void submit(JobHandle jobHandle)
Pushes jobHandle into the internal queue so worker threads can pick it up and execute it....
Definition threadpool.h:501
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:294
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:370
void dep_refresh(JobHandle jobFirst, JobHandle jobSecond)
Makes jobSecond depend on jobFirst. This means jobSecond will not run until jobFirst finishes.
Definition threadpool.h:357
static ThreadPool & get()
Returns the process-wide thread-pool instance.
Definition threadpool.h:161
JobHandle sched(Job job)
Schedules a job to run on a worker thread.
Definition threadpool.h:579
void reset_state(JobHandle jobHandle)
Resets a completed job to the clear reusable state without waiting.
Definition threadpool.h:533
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:466
JobHandle sched(Job job, JobHandle dependsOn)
Schedules a job to run on a worker thread.
Definition threadpool.h:606
void reset_state(std::span< JobHandle > jobHandles)
Resets completed jobs to the clear reusable state without waiting.
Definition threadpool.h:519
static GAIA_NODISCARD uint32_t hw_efficiency_cores_cnt()
Returns the number of efficiency cores of the system.
Definition threadpool.h:881
void make_main_thread()
Make the calling thread the effective main thread from the thread pool perspective.
Definition threadpool.h:171
void wait(JobHandle jobHandle)
Wait until a job associated with the jobHandle finishes executing. Cleans up any job allocations and ...
Definition threadpool.h:814
void set_workers_low_prio(uint32_t count)
Updates the number of worker threads participating at low priority workloads.
Definition threadpool.h:279
void del(JobHandle jobHandle)
Deletes a job handle jobHandle from the threadpool.
Definition threadpool.h:441
void reset(JobHandle jobHandle)
Waits for jobHandle to finish and resets it to a reusable state.
Definition threadpool.h:570
void set_workers_high_prio(uint32_t count)
Updates the number of worker threads participating at high priority workloads.
Definition threadpool.h:267
GAIA_NODISCARD uint32_t workers() const
Returns the number of frame worker threads.
Definition threadpool.h:179
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:344
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:253
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:593
static GAIA_NODISCARD uint32_t hw_thread_cnt()
Returns the number of HW threads available on the system. 1 is minimum.
Definition threadpool.h:874
static SmallFunc create(F &&f)
Creates a wrapper from a callable compatible with void().
Definition small_func.h:194
RAII helper that calls lock() on construction and unlock() on destruction.
Definition utility.h:188
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:56
uint32_t idxStart
First item index processed by this invocation.
Definition jobcommon.h:58
uint32_t idxEnd
One-past-the-last item index processed by this invocation.
Definition jobcommon.h:60
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:217
void(* invoke)(void *, const JobArgs &)
Function that invokes the callback stored in the context.
Definition jobcommon.h:221
void * pCtx
Non-owning callback context.
Definition jobcommon.h:219
JobPriority priority
Queue priority used for each range job.
Definition jobcommon.h:223
Callable and priority for a range-partitioned parallel job.
Definition jobcommon.h:208
JobPriority priority
Queue priority used for each range job.
Definition jobcommon.h:212
JobArgsFunc func
Callable invoked once for each scheduled range.
Definition jobcommon.h:210
Callable and scheduling options for a single job.
Definition jobcommon.h:46
JobCreationFlags flags
Creation and lifetime options.
Definition jobcommon.h:52
Per-thread execution state owned by ThreadPool.
Definition jobcommon.h:229