diff options
| author | Léo Lam <leo@leolam.fr> | 2020-09-10 22:11:27 +0200 |
|---|---|---|
| committer | Léo Lam <leo@leolam.fr> | 2020-09-15 17:54:16 +0200 |
| commit | 8b7369dffb1e816b2f27ab0133d2bac218383a45 (patch) | |
| tree | c5d6a577ec9102e65a6e08272432dfa862670a60 /src/KingSystem/Utils/Thread | |
| parent | c5ef0a1fd3629a27dc2fc7de7a46347c6cb3518e (diff) | |
ksys: Add Task utilities
Diffstat (limited to 'src/KingSystem/Utils/Thread')
| -rw-r--r-- | src/KingSystem/Utils/Thread/Task.cpp | 277 | ||||
| -rw-r--r-- | src/KingSystem/Utils/Thread/Task.h | 193 | ||||
| -rw-r--r-- | src/KingSystem/Utils/Thread/TaskQueue.cpp | 7 | ||||
| -rw-r--r-- | src/KingSystem/Utils/Thread/TaskQueue.h | 21 | ||||
| -rw-r--r-- | src/KingSystem/Utils/Thread/TaskQueueBase.cpp | 491 | ||||
| -rw-r--r-- | src/KingSystem/Utils/Thread/TaskQueueBase.h | 157 | ||||
| -rw-r--r-- | src/KingSystem/Utils/Thread/TaskQueueLock.cpp | 26 | ||||
| -rw-r--r-- | src/KingSystem/Utils/Thread/TaskQueueLock.h | 28 | ||||
| -rw-r--r-- | src/KingSystem/Utils/Thread/TaskThread.cpp | 261 | ||||
| -rw-r--r-- | src/KingSystem/Utils/Thread/TaskThread.h | 109 |
10 files changed, 1570 insertions, 0 deletions
diff --git a/src/KingSystem/Utils/Thread/Task.cpp b/src/KingSystem/Utils/Thread/Task.cpp new file mode 100644 index 00000000..7ab00b55 --- /dev/null +++ b/src/KingSystem/Utils/Thread/Task.cpp @@ -0,0 +1,277 @@ +#include "KingSystem/Utils/Thread/Task.h" +#include <thread/seadThread.h> +#include "KingSystem/Utils/Thread/TaskQueueBase.h" +#include "KingSystem/Utils/Thread/TaskQueueLock.h" +#include "KingSystem/Utils/Thread/TaskThread.h" + +namespace ksys::util { + +TaskDelegateSetter::TaskDelegateSetter() = default; + +TaskDelegateSetter::~TaskDelegateSetter() = default; + +void TaskDelegateSetter::setDelegate(TaskDelegate* delegate) { + mDelegate = delegate; +} + +Task::Task(sead::Heap* heap) : mEvent(heap, true) { + mEvent.setSignal(); +} + +Task::Task(sead::Heap* heap, sead::IDisposer::HeapNullOption heap_null_option) + : mEvent(heap, heap_null_option, true) { + mEvent.setSignal(); +} + +Task::~Task() { + finalize_(); +} + +void Task::deleteDelegate_() { + if (mDelegate && mFlags.isOn(Flag::DeleteDelegate) && mFlags.isOff(Flag::DoNotDeleteDelegate)) { + delete mDelegate; + mDelegate = nullptr; + } +} + +// NON_MATCHING: mDelegate2 = nullptr store +void Task::finalize_() { + if (mStatus == Status::Finalized) + return; + + removeFromQueue(); + deleteDelegate_(); + mUserData = nullptr; + mQueue = nullptr; + mPostRunCallback = nullptr; + mRemoveCallback = nullptr; + mStatus = Status::Finalized; +} + +bool Task::setDelegate(const TaskDelegateSetter& setter) { + mDelegate = setter.getDelegate(); + mFlags.reset(Flag::DeleteDelegate); + mFlags.reset(Flag::DoNotDeleteDelegate); + mFlags.set(Flag::DoNotDeleteDelegate); + return onSetDelegate_(setter); +} + +// NON_MATCHING: branching +bool Task::submitRequest(TaskRequest& request) { + // Processing this request is impossible if there is no thread *and* no queue! + if (request.mThread == nullptr && request.mQueue == nullptr) + return false; + + if (!canSubmitRequest()) + return false; + + if (request.mSynchronous || request.mHasHandle) + mFlags.set(Flag::NeedsToSignalEvent); + else + mFlags.reset(Flag::NeedsToSignalEvent); + + mFlags.change(Flag::SynchronousRequest, request.mSynchronous); + + if (mListNode.isLinked()) + return false; + + if (mFlags.isOn(Flag::NeedsToSignalEvent)) + mEvent.resetSignal(); + + mQueue = request.mQueue; + if (!mQueue) { + mQueue = request.mThread->getTaskQueue(); + request.mQueue = mQueue; + } + mUserData = request.mUserData; + if (auto* delegate = request.mDelegate) { + deleteDelegate_(); + mFlags.set(Flag::DoNotDeleteDelegate); + mDelegate = delegate; + } + mRemoveCallback = request.mRemoveCallback; + mPostRunCallback = request.mPostRunCallback; + mName = request.mName; + + prepare_(&request); + + if (request.mSynchronous) { + auto* thread = mQueue->getCurrentThread(); + if (thread) { + processOnCurrentThreadDirectly(thread); + return true; + } + } + + TaskQueueBase::PushArg arg; + arg.lane_id = request.mLaneId; + arg.task = this; + const bool push_ok = mQueue->push(arg); + bool b; + if (push_ok) { + if (request.mSynchronous) + mEvent.wait(); + b = true; + } else { + b = false; + } + return push_ok || b; +} + +bool Task::canSubmitRequest() const { + const bool run_finished_on_current_thread = + mThread && mStatus == Status::RunFinished && + mThread == sead::ThreadMgr::instance()->getCurrentThread(); + const bool cond2 = isInactive(); + return run_finished_on_current_thread || cond2; +} + +void Task::processOnCurrentThreadDirectly(TaskThread* thread) { + { + TaskQueueLock lock{thread}; + mThread = thread; + mStatus = Status::Pushed; + } + + run(); + + { + TaskQueueLock lock{thread}; + onRunFinished(); + } + + TaskPostRunResult result; + invokePostRunCallback(&result); + + { + TaskQueueLock lock{thread}; + if (!result.getResult()) + finish(); + } +} + +void Task::removeFromQueue() { + if (mQueue) + mQueue->removeTask(this, true); +} + +void Task::removeFromQueue2() { + // TODO: how does this differ from removeFromQueue? + removeFromQueue(); +} + +void Task::run_() { + if (mDelegate) + mDelegateResult = (*mDelegate)(mUserData); +} + +bool Task::wait() { + mEvent.wait(); + return true; +} + +bool Task::wait(const sead::TickSpan& span) { + return mEvent.wait(span); +} + +u8 Task::getLaneId() const { + return mLaneId; +} + +bool Task::isSuccess() const { + return (mStatus == Status::PreFinishCallback || mStatus == Status::RunFinished || + mStatus == Status::PostFinishCallback) && + mDelegateResult; +} + +bool Task::isInactive() const { + return mStatus == Status::Uninitialized || mStatus == Status::RemovedFromQueue || + mStatus == Status::PostFinishCallback; +} + +void Task::setStatusPushed() { + mStatus = Status::Pushed; +} + +void Task::setThread(TaskThread* thread) { + mThread = thread; +} + +void Task::run() { + run_(); + mStatus = Status::RunFinished; +} + +void Task::onRunFinished() { + onRunFinished_(); +} + +void Task::invokePostRunCallback(TaskPostRunResult* result) { + mRemoveCallback = nullptr; + TaskPostRunContext context; + context.mCancelled = mFlags.isOn(Flag::Cancelled); + context.mTask = this; + context.mUserData = mUserData; + if (auto* delegate = mPostRunCallback) { + mPostRunCallback = nullptr; + delegate->invoke(result, context); + } +} + +void Task::finish() { + mStatus = Status::PreFinishCallback; + onFinish_(); + mStatus = Status::PostFinishCallback; + + mThread = nullptr; + signalEvent(); + + onPostFinish_(); +} + +void* Task::getUserData() const { + return mUserData; +} + +void Task::cancel() { + mEvent.resetSignal(); + mFlags.set(Flag::Cancelled); + mFlags.set(Flag::NeedsToSignalEvent); +} + +void Task::onRemove() { + invokeRemoveCallback_(); + + TaskQueueLock lock; + mQueue->lock(&lock); + + preRemove_(); + + mStatus = Status::RemovedFromQueue; + mThread = nullptr; + signalEvent(); + + postRemove_(); +} + +void Task::invokeRemoveCallback_() { + mPostRunCallback = nullptr; + TaskRemoveCallbackContext context; + context.mTask = this; + context.mUserData = mUserData; + + if (auto* delegate = mRemoveCallback) { + mRemoveCallback = nullptr; + delegate->invoke(context); + } +} + +void Task::setStatusFetched() { + mStatus = Status::Fetched; +} + +void Task::setLaneId(u8 id) { + mLaneId = id; +} + +} // namespace ksys::util diff --git a/src/KingSystem/Utils/Thread/Task.h b/src/KingSystem/Utils/Thread/Task.h new file mode 100644 index 00000000..df54db8e --- /dev/null +++ b/src/KingSystem/Utils/Thread/Task.h @@ -0,0 +1,193 @@ +#pragma once + +#include <basis/seadTypes.h> +#include <container/seadListImpl.h> +#include <prim/seadDelegate.h> +#include <prim/seadRuntimeTypeInfo.h> +#include <prim/seadSafeString.h> +#include <prim/seadTypedBitFlag.h> +#include "KingSystem/Utils/Thread/Event.h" +#include "KingSystem/Utils/Types.h" + +namespace ksys::util { + +class Task; +class TaskQueueBase; +class TaskRequest; +class TaskThread; + +class TaskPostRunResult { + SEAD_RTTI_BASE(TaskPostRunResult) +public: + virtual ~TaskPostRunResult() = default; + + bool getResult() const { return mResult; } + void setResult(bool result) { mResult = result; } + +private: + bool mResult = false; +}; +KSYS_CHECK_SIZE_NX150(TaskPostRunResult, 0x10); + +class TaskPostRunContext { + SEAD_RTTI_BASE(TaskPostRunContext) +public: + virtual ~TaskPostRunContext() = default; + + bool mCancelled; + Task* mTask; + void* mUserData; +}; +KSYS_CHECK_SIZE_NX150(TaskPostRunContext, 0x20); + +class TaskRemoveCallbackContext { + SEAD_RTTI_BASE(TaskRemoveCallbackContext) +public: + virtual ~TaskRemoveCallbackContext() = default; + + Task* mTask; + void* mUserData; +}; +KSYS_CHECK_SIZE_NX150(TaskRemoveCallbackContext, 0x18); + +using TaskDelegate = sead::AnyDelegate1R<void*, bool>; +using TaskPostRunCallback = sead::IDelegate2<TaskPostRunResult*, const TaskPostRunContext&>; +using TaskRemoveCallback = sead::IDelegate1<const TaskRemoveCallbackContext&>; + +class TaskDelegateSetter { + SEAD_RTTI_BASE(TaskDelegateSetter) +public: + TaskDelegateSetter(); + explicit TaskDelegateSetter(TaskDelegate* delegate) : TaskDelegateSetter() { + setDelegate(delegate); + } + virtual ~TaskDelegateSetter(); + TaskDelegate* getDelegate() const { return mDelegate; } + void setDelegate(TaskDelegate* delegate); + +private: + TaskDelegate* mDelegate = nullptr; +}; +KSYS_CHECK_SIZE_NX150(TaskDelegateSetter, 0x10); + +class TaskRequest { + SEAD_RTTI_BASE(TaskRequest) +public: + virtual ~TaskRequest() = default; + + bool mHasHandle; + /// If true, request submissions will block until the request is processed. + bool mSynchronous; + u8 mLaneId; + TaskThread* mThread; + TaskQueueBase* mQueue; + TaskDelegate* mDelegate; + void* mUserData; + TaskRemoveCallback* mRemoveCallback; + TaskPostRunCallback* mPostRunCallback; + sead::SafeString mName; +}; +KSYS_CHECK_SIZE_NX150(TaskRequest, 0x50); + +class Task { + SEAD_RTTI_BASE(Task) +public: + enum class Status { + Uninitialized = 0, + RemovedFromQueue = 1, + Pushed = 2, + Fetched = 3, + PreFinishCallback = 4, + RunFinished = 5, + PostFinishCallback = 6, + Finalized = 7, + }; + + explicit Task(sead::Heap* heap); + Task(sead::Heap* heap, sead::IDisposer::HeapNullOption heap_null_option); + virtual ~Task(); + + bool setDelegate(const TaskDelegateSetter& setter); + bool submitRequest(TaskRequest& request); + bool canSubmitRequest() const; + void processOnCurrentThreadDirectly(TaskThread* thread); + + void removeFromQueue(); + void removeFromQueue2(); + + void cancel(); + bool wait(); + bool wait(const sead::TickSpan& span); + + Status getStatus() const { return mStatus; } + bool isSuccess() const; + bool isInactive() const; + + u8 getLaneId() const; + void* getUserData() const; + TaskQueueBase* getQueue() const { return mQueue; } + +protected: + friend class TaskQueueBase; + friend class TaskThread; + + enum class Flag { + DeleteDelegate = 0x1, + DoNotDeleteDelegate = 0x2, + NeedsToSignalEvent = 0x4, + SynchronousRequest = 0x8, + Cancelled = 0x10, + }; + + static size_t getListNodeOffset() { return offsetof(Task, mListNode); } + + virtual bool onSetDelegate_(const TaskDelegateSetter&) { return true; } + virtual void prepare_(TaskRequest* request); + virtual void run_(); + virtual void onRunFinished_() {} + virtual void onFinish_() {} + virtual void onPostFinish_() {} + virtual void preRemove_() {} + virtual void postRemove_() {} + + void setLaneId(u8 id); + void setThread(TaskThread* thread); + void setStatusPushed(); + void setStatusFetched(); + void run(); + void onRunFinished(); + void invokePostRunCallback(TaskPostRunResult* result); + void finish(); + void onRemove(); + + void finalize_(); + void deleteDelegate_(); + void invokeRemoveCallback_(); + + void signalEvent() { + if (!mFlags.isOn(Flag::NeedsToSignalEvent)) + return; + + mFlags.reset(Flag::Cancelled); + mEvent.setSignal(); + } + + u8 mLaneId = 0; + sead::TypedBitFlag<Flag, u8> mFlags = Flag::DoNotDeleteDelegate; + bool mDelegateResult = false; + TaskDelegate* mDelegate = nullptr; + void* mUserData = nullptr; + TaskQueueBase* mQueue = nullptr; + TaskThread* mThread = nullptr; + TaskPostRunCallback* mPostRunCallback = nullptr; + TaskRemoveCallback* mRemoveCallback = nullptr; + Status mStatus = Status::Uninitialized; + sead::ListNode mListNode; + Event mEvent; + sead::SafeString mName; +}; +KSYS_CHECK_SIZE_NX150(Task, 0xa8); + +inline void Task::prepare_(TaskRequest*) {} + +} // namespace ksys::util diff --git a/src/KingSystem/Utils/Thread/TaskQueue.cpp b/src/KingSystem/Utils/Thread/TaskQueue.cpp new file mode 100644 index 00000000..cf76c768 --- /dev/null +++ b/src/KingSystem/Utils/Thread/TaskQueue.cpp @@ -0,0 +1,7 @@ +#include "KingSystem/Utils/Thread/TaskQueue.h" + +namespace ksys::util { + +TaskQueue::TaskQueue(sead::Heap* heap) : TaskQueueBase(heap), mCS(heap) {} + +} // namespace ksys::util diff --git a/src/KingSystem/Utils/Thread/TaskQueue.h b/src/KingSystem/Utils/Thread/TaskQueue.h new file mode 100644 index 00000000..6792a783 --- /dev/null +++ b/src/KingSystem/Utils/Thread/TaskQueue.h @@ -0,0 +1,21 @@ +#pragma once + +#include <thread/seadCriticalSection.h> +#include "KingSystem/Utils/Thread/TaskQueueBase.h" + +namespace ksys::util { + +class TaskQueue : public TaskQueueBase { + SEAD_RTTI_OVERRIDE(TaskQueue, TaskQueueBase) +public: + explicit TaskQueue(sead::Heap* heap); + +private: + void lock() const override { mCS.lock(); } + void unlock() const override { mCS.unlock(); } + + mutable sead::CriticalSection mCS; +}; +KSYS_CHECK_SIZE_NX150(TaskQueue, 0xd0); + +} // namespace ksys::util diff --git a/src/KingSystem/Utils/Thread/TaskQueueBase.cpp b/src/KingSystem/Utils/Thread/TaskQueueBase.cpp new file mode 100644 index 00000000..430fdbf2 --- /dev/null +++ b/src/KingSystem/Utils/Thread/TaskQueueBase.cpp @@ -0,0 +1,491 @@ +#include "KingSystem/Utils/Thread/TaskQueueBase.h" +#include <algorithm> +#include <memory> +#include <thread/seadThread.h> +#include "KingSystem/Utils/Debug.h" +#include "KingSystem/Utils/Thread/Task.h" +#include "KingSystem/Utils/Thread/TaskQueueLock.h" +#include "KingSystem/Utils/Thread/TaskThread.h" + +namespace ksys::util { + +static const auto cSleepSpan = sead::TickSpan::fromMicroSeconds(10); + +TaskQueueBase::TaskQueueBase(sead::Heap* heap) : mQueueEmptyEvent(heap) { + mActiveTasks.initOffset(Task::getListNodeOffset()); +} + +TaskQueueBase::~TaskQueueBase() { + clear(); + + for (auto& lane : mLanes) { + delete lane.lane_empty_event; + lane.lane_empty_event = nullptr; + } + + mLanes.freeBuffer(); + mThreads.freeBuffer(); +} + +void TaskQueueBase::clear() { + lock(); + + // Clear all tasks. + for (auto it = mActiveTasks.robustBegin(), end = mActiveTasks.robustEnd(); it != end; ++it) { + mActiveTasks.erase(std::addressof(*it)); + it->onRemove(); + } + mActiveTasks.clear(); + + for (auto& lane : mLanes) + lane.head_task = nullptr; + + const bool is_any_thread_busy = isAnyThreadBusy(); + + signalEmptyEventsIfNeeded(); + { + ConditionalScopedLock lock{this}; + ScopedLock lock1{this}; + for (auto& thread : mThreads) { + thread.cancelCurrentTask(); + } + } + + unlock(); + + if (is_any_thread_busy) + mQueueEmptyEvent.wait(); +} + +// NON_MATCHING: swapped operands for a csel. The arg.set_flag1 check looks suspicious. +bool TaskQueueBase::init(const InitArg& arg) { + if (arg.max_num_threads == 0) + return false; + + if (!mThreads.tryAllocBuffer(arg.max_num_threads, arg.heap)) + return false; + + if (!arg.enable_locks) + mFlags.reset(Flag::Lock); + else + mFlags.set(Flag::Lock); + + if (arg.num_lanes <= 0 || arg.num_lanes > 0x100) + return false; + + mLanes.allocBufferAssert(arg.num_lanes, arg.heap); + for (auto& lane : mLanes) { + lane.lane_empty_event = new (arg.heap) Event(arg.heap, true); + lane.lane_empty_event->setSignal(); + } + + mQueueEmptyEvent.initialize(true); + mQueueEmptyEvent.setSignal(); + mTaskSelectionDelegate = arg.task_selection_delegate; + + return true; +} + +bool TaskQueueBase::addThread(TaskThread* thread) { + if (mFlags.isOn(Flag::PreventThreadPoolChanges)) + return false; + + lockIfNeeded(); + + if (mThreads.isFull()) { + unlockIfNeeded(); + return false; + } + + mThreads.pushBack(thread); + unlockIfNeeded(); + return true; +} + +void TaskQueueBase::removeThread(TaskThread* thread) { + if (mFlags.isOn(Flag::PreventThreadPoolChanges)) + return; + + ConditionalScopedLock lock{this}; + mThreads.erase(mThreads.search(thread)); +} + +s32 TaskQueueBase::getNumActiveTasks() const { + return mActiveTasks.size(); +} + +s32 TaskQueueBase::countTasksInLane(u16 id) const { + lock(); + + if (!mLanes[id].head_task) { + unlock(); + return 0; + } + + s32 count = 0; + for (auto i = mActiveTasks.begin(mLanes[id].head_task), end = mActiveTasks.end(); i != end; + ++i) { + if (i->getLaneId() != id) + break; + ++count; + } + unlock(); + return count; +} + +bool TaskQueueBase::areNoThreadsBusy() const { + ScopedLock lock{this}; + + if (!mActiveTasks.isEmpty()) + return false; + return !isAnyThreadBusy(); +} + +bool TaskQueueBase::isAnyThreadBusy() const { + ConditionalScopedLock lock{this}; + return std::any_of(mThreads.begin(), mThreads.end(), + [](const TaskThread& thread) { return thread.isBusyProcessingTask(); }); +} + +bool TaskQueueBase::areAllThreadsPaused() const { + ConditionalScopedLock lock{this}; + return std::all_of(mThreads.begin(), mThreads.end(), + [](const TaskThread& thread) { return thread.isPaused(); }); +} + +void TaskQueueBase::waitForQueueToEmpty() { + if (areAllThreadsPaused()) + return; + + for (const auto& lane : mLanes) { + if (lane.blocked && lane.head_task) + return; + } + + mQueueEmptyEvent.wait(); +} + +void TaskQueueBase::waitForLaneToEmpty(u8 id) { + if (areAllThreadsPaused()) + return; + + const auto& lane = mLanes[id]; + if (!lane.blocked || !lane.head_task) + lane.lane_empty_event->wait(); +} + +void TaskQueueBase::cancelTasks(u8 id) { + lock(); + + if (mLanes[id].head_task) { + for (auto it = mActiveTasks.robustBegin(mLanes[id].head_task), + end = mActiveTasks.robustEnd(); + it != end; ++it) { + if (it->getLaneId() != id) + break; + mActiveTasks.erase(std::addressof(*it)); + it->onRemove(); + } + } + mLanes[id].head_task = nullptr; + + const auto cancel_current_tasks_if_needed = [&] { + ConditionalScopedLock lock{this}; + ScopedLock lock1{this}; + for (auto it = mThreads.begin(), end = mThreads.end(); it != end; ++it) { + if (it->mTask && it->mTask->getLaneId() == id) + it->cancelCurrentTask(); + } + }; + + if (isProcessingTask(id)) { + mLanes[id].lane_empty_event->resetSignal(); + signalEmptyEventsIfNeeded(); + cancel_current_tasks_if_needed(); + unlock(); + mLanes[id].lane_empty_event->wait(); + } else { + signalEmptyEventsIfNeeded(); + cancel_current_tasks_if_needed(); + unlock(); + } +} + +bool TaskQueueBase::isProcessingTask(u8 id) const { + ConditionalScopedLock lock{this}; + ScopedLock lock1{this}; + return std::any_of(mThreads.begin(), mThreads.end(), [id](const TaskThread& thread) { + return thread.mTask && thread.mTask->getLaneId() == id; + }); +} + +void TaskQueueBase::signalEmptyEventsIfNeeded() { + ScopedLock lock{this}; + + const bool is_any_thread_busy = isAnyThreadBusy(); + const bool has_no_tasks = mActiveTasks.isEmpty(); + if (!is_any_thread_busy && has_no_tasks) + mQueueEmptyEvent.setSignal(); + + for (auto it = mLanes.begin(), end = mLanes.end(); it != end; ++it) { + if (!isProcessingTask(it.getIndex()) && it->head_task == nullptr) + it->lane_empty_event->setSignal(); + } +} + +void TaskQueueBase::blockTasks(u8 id) { + if (mLanes[id].blocked != 1) + mLanes[id].blocked = true; +} + +// NON_MATCHING: the while (!areAllThreadsPaused()) loop generates weird code in the original +void TaskQueueBase::blockTasksAndReloadThreads(u8 id) { + blockTasks(id); + + { + ConditionalScopedLock lock{this}; + for (auto& thread : mThreads) + thread.pause(); + } + + const auto sleep_duration = sead::TickSpan::fromMilliSeconds(1); + + while (!areAllThreadsPaused()) + sead::Thread::sleep(sleep_duration); + + sead::Thread::sleep(sleep_duration); + + { + ConditionalScopedLock lock{this}; + for (auto& thread : mThreads) + thread.resume(); + } +} + +void TaskQueueBase::unblockTasks(u8 id) { + if (mLanes[id].blocked) { + mLanes[id].blocked = false; + notifyThreadsForNewTasks(); + } +} + +void TaskQueueBase::lock(TaskQueueLock* lock) { + lock->lock(this); +} + +TaskThread* TaskQueueBase::getCurrentThread() const { + const sead::Thread* current_thread = sead::ThreadMgr::instance()->getCurrentThread(); + lockIfNeeded(); + for (auto it = mThreads.begin(), end = mThreads.end(); it != end; ++it) { + if (current_thread == std::addressof(*it)) { + unlockIfNeeded(); + return std::addressof(*it); + } + } + unlockIfNeeded(); + return nullptr; +} + +sead::OffsetList<Task>::iterator TaskQueueBase::activeTasksBegin(TaskQueueLock* lock) { + lock->lock(this); + return mActiveTasks.begin(); +} + +sead::OffsetList<Task>::robustIterator TaskQueueBase::activeTasksRobustBegin(TaskQueueLock* lock) { + lock->lock(this); + return mActiveTasks.robustBegin(); +} + +sead::OffsetList<Task>::iterator TaskQueueBase::activeTasksEnd() const { + return mActiveTasks.end(); +} + +sead::OffsetList<Task>::robustIterator TaskQueueBase::activeTasksRobustEnd() const { + return mActiveTasks.robustEnd(); +} + +void TaskQueueBase::notifyThreadsForNewTasks() { + s32 retry_count = 0; + const sead::Thread* current_thread = sead::ThreadMgr::instance()->getCurrentThread(); + sead::BitFlag32 mask = 0; + + while (true) { + lockIfNeeded(); + bool done = true; + s32 i = 0; + auto* data = mThreads.data(); + for (auto& thread : mThreads) { + static_cast<void>(thread); + if (current_thread != data[i] && !data[i]->isLookingForTask() && + !data[i]->receivedQueueUpdatedMsg() && !data[i]->receivedPauseMsg() && + !data[i]->receivedResumeMsg() && !data[i]->receivedQuitMsg() && !mask.isOnBit(i)) { + const bool send_ok = data[i]->sendMessage( + TaskThread::cMessage_QueueUpdated, sead::MessageQueue::BlockType::NonBlocking); + if (send_ok) + mask.setBit(i); + done &= send_ok; + } + ++i; + } + unlockIfNeeded(); + + if (done) + break; + + ++retry_count; + sead::Thread::sleep(cSleepSpan); + } + + if (retry_count >= 2) + PrintDebug(sead::FormatFixedSafeString<128>("↓↓↓\nリトライ回数 %d 回\n↑↑↑\n", retry_count)); +} + +// NON_MATCHING: regalloc for max_idx +bool TaskQueueBase::push(const PushArg& arg) { + lock(); + + if (!arg.task || mActiveTasks.isNodeLinked(arg.task)) { + unlock(); + return false; + } + + const auto num_tasks = mActiveTasks.size(); + const u8 max_idx = mLanes.size() - 1; + const u8 id = arg.lane_id <= max_idx ? arg.lane_id : max_idx; + arg.task->setLaneId(id); + + bool added = false; + for (u8 i = id - 1; i != 0xff; --i) { + if (!mLanes[i].head_task) + continue; + + mActiveTasks.insertBefore(mLanes[i].head_task, arg.task); + added = true; + break; + } + + if (!added) { + mActiveTasks.pushBack(arg.task); + mLanes[id].lane_empty_event->resetSignal(); + } + + if (!mLanes[id].head_task) + mLanes[id].head_task = arg.task; + + arg.task->setStatusPushed(); + + if (num_tasks == 0) + mQueueEmptyEvent.resetSignal(); + + unlock(); + notifyThreadsForNewTasks(); + return true; +} + +void TaskQueueBase::removeTask(Task* task, bool b) { + if (!task) + return; + + lock(); + + if (!task->isInactive()) { + if (task->getStatus() == Task::Status::Pushed) { + const u8 id = task->getLaneId(); + if (mLanes[id].head_task == task) { + auto* new_task = mActiveTasks.next(task); + if (new_task && task->getLaneId() == new_task->getLaneId()) + mLanes[id].head_task = new_task; + else + mLanes[id].head_task = nullptr; + } + + mActiveTasks.erase(task); + task->onRemove(); + signalEmptyEventsIfNeeded(); + } else if (b) { + task->cancel(); + unlock(); + task->wait(); + return; + } + } + + unlock(); +} + +// NON_MATCHING: regalloc inside the task lambda + reorderings for the loop counters. +void TaskQueueBase::fetchTask(Task** out_task) { + lock(); + + const auto check_state = [&] { + if (!mActiveTasks.isEmpty() || isAnyThreadBusy()) + return true; + mQueueEmptyEvent.setSignal(); + *out_task = nullptr; + unlock(); + return false; + }; + + if (!check_state()) + return; + + auto* task = [&]() -> Task* { + for (auto it = mLanes.rbegin(), end = mLanes.rend(); it != end; ++it) { + if (it->blocked) + continue; + if (it->head_task == nullptr) + continue; + if (!mTaskSelectionDelegate) + return it->head_task; + + const auto it_begin = mActiveTasks.begin(it->head_task); + Task* end_ptr = nullptr; + for (auto it2 = it; it2 != mLanes.rbegin(0);) { + // XXX: This looks really weird. + auto* t = std::addressof(*it2)[-1].head_task; + ++it2; + if (t) { + if (it2->head_task) + end_ptr = it2->head_task; + break; + } + } + const auto it_end = end_ptr ? mActiveTasks.begin(end_ptr) : mActiveTasks.end(); + + TaskSelectionContext context; + context.lane_id = it->head_task->getLaneId(); + context.it_begin = &it_begin; + context.it_end = &it_end; + Task* task = mTaskSelectionDelegate->invoke(context); + if (task) + return task; + } + + return nullptr; + }(); + + if (!check_state()) + return; + + if (task) { + for (u8 id = mLanes.size() - 1; id != 0xff; --id) { + if (mLanes[id].head_task == task) { + auto* new_task = mActiveTasks.next(task); + if (new_task && task->getLaneId() == new_task->getLaneId()) + mLanes[id].head_task = new_task; + else + mLanes[id].head_task = nullptr; + break; + } + } + mActiveTasks.erase(task); + task->setStatusFetched(); + *out_task = task; + } else { + *out_task = nullptr; + } + + unlock(); +} + +} // namespace ksys::util diff --git a/src/KingSystem/Utils/Thread/TaskQueueBase.h b/src/KingSystem/Utils/Thread/TaskQueueBase.h new file mode 100644 index 00000000..3d4b44cf --- /dev/null +++ b/src/KingSystem/Utils/Thread/TaskQueueBase.h @@ -0,0 +1,157 @@ +#pragma once + +#include <basis/seadTypes.h> +#include <container/seadBuffer.h> +#include <container/seadOffsetList.h> +#include <container/seadPtrArray.h> +#include <prim/seadDelegate.h> +#include <prim/seadRuntimeTypeInfo.h> +#include <prim/seadTypedBitFlag.h> +#include <time/seadTickSpan.h> +#include "KingSystem/Utils/Thread/Event.h" +#include "KingSystem/Utils/Types.h" + +namespace ksys::util { + +class Task; +class TaskQueueLock; +class TaskThread; + +struct TaskSelectionContext { + const auto& begin() const { return *it_begin; } + const auto& end() const { return *it_end; } + u8 lane_id; + const sead::OffsetList<Task>::iterator* it_begin; + const sead::OffsetList<Task>::iterator* it_end; +}; + +using TaskSelectionDelegate = sead::IDelegate1R<const TaskSelectionContext&, Task*>; + +class TaskQueueBase { + SEAD_RTTI_BASE(TaskQueueBase) +public: + struct InitArg { + bool enable_locks; + /// Number of lanes. + u16 num_lanes; + /// Maximum number of threads that will be processing the queue. + u16 max_num_threads; + sead::Heap* heap; + TaskSelectionDelegate* task_selection_delegate; + }; + KSYS_CHECK_SIZE_NX150(InitArg, 0x18); + + struct PushArg { + u8 lane_id; + Task* task; + }; + KSYS_CHECK_SIZE_NX150(PushArg, 0x10); + + explicit TaskQueueBase(sead::Heap* heap); + virtual ~TaskQueueBase(); + + void clear(); + bool init(const InitArg& arg); + + bool addThread(TaskThread* thread); + void removeThread(TaskThread* thread); + + s32 getNumActiveTasks() const; + s32 countTasksInLane(u16 id) const; + bool areNoThreadsBusy() const; + bool isAnyThreadBusy() const; + bool areAllThreadsPaused() const; + + void waitForQueueToEmpty(); + void waitForLaneToEmpty(u8 id); + + void cancelTasks(u8 id); + bool isProcessingTask(u8 id) const; + void signalEmptyEventsIfNeeded(); + + void blockTasks(u8 id); + void blockTasksAndReloadThreads(u8 id); + void unblockTasks(u8 id); + + void lock(TaskQueueLock* lock); + + /// @returns the current thread if it is in the thread pool and nullptr otherwise. + TaskThread* getCurrentThread() const; + + sead::OffsetList<Task>::iterator activeTasksBegin(TaskQueueLock* lock); + sead::OffsetList<Task>::robustIterator activeTasksRobustBegin(TaskQueueLock* lock); + sead::OffsetList<Task>::iterator activeTasksEnd() const; + sead::OffsetList<Task>::robustIterator activeTasksRobustEnd() const; + + bool push(const PushArg& arg); + void removeTask(Task* task, bool b); + void fetchTask(Task** out_task); + +protected: + enum class Flag { + Lock = 0x1, + PreventThreadPoolChanges = 0x2, + }; + + struct Lane { + /// If true, tasks in this lane are not allowed to be fetched by any thread. + bool blocked = false; + /// First task in the lane. Tasks are also added to a linked list (mActiveTasks). + Task* head_task = nullptr; + Event* lane_empty_event = nullptr; + }; + KSYS_CHECK_SIZE_NX150(Lane, 0x18); + + class ScopedLock { + public: + explicit ScopedLock(const TaskQueueBase* queue) : mQueue(queue) { mQueue->lock(); } + ScopedLock(const ScopedLock&) = delete; + ~ScopedLock() { mQueue->unlock(); } + ScopedLock& operator=(const ScopedLock&) = delete; + + private: + const TaskQueueBase* mQueue; + }; + + class ConditionalScopedLock { + public: + explicit ConditionalScopedLock(const TaskQueueBase* queue) : mQueue(queue) { + mQueue->lockIfNeeded(); + } + ConditionalScopedLock(const ConditionalScopedLock&) = delete; + ~ConditionalScopedLock() { mQueue->unlockIfNeeded(); } + ConditionalScopedLock& operator=(const ConditionalScopedLock&) = delete; + + private: + const TaskQueueBase* mQueue; + }; + + friend class TaskQueueLock; + + virtual void lock() const {} + virtual void unlock() const {} + + bool shouldLock() const { return mFlags.isOn(Flag::Lock); } + + void lockIfNeeded() const { + if (shouldLock()) + lock(); + } + + void unlockIfNeeded() const { + if (shouldLock()) + unlock(); + } + + void notifyThreadsForNewTasks(); + + sead::TypedBitFlag<Flag, u8> mFlags; + sead::OffsetList<Task> mActiveTasks; + sead::Buffer<Lane> mLanes; + Event mQueueEmptyEvent; + sead::PtrArray<TaskThread> mThreads; + TaskSelectionDelegate* mTaskSelectionDelegate = nullptr; +}; +KSYS_CHECK_SIZE_NX150(TaskQueueBase, 0x90); + +} // namespace ksys::util diff --git a/src/KingSystem/Utils/Thread/TaskQueueLock.cpp b/src/KingSystem/Utils/Thread/TaskQueueLock.cpp new file mode 100644 index 00000000..be7b1c5e --- /dev/null +++ b/src/KingSystem/Utils/Thread/TaskQueueLock.cpp @@ -0,0 +1,26 @@ +#include "KingSystem/Utils/Thread/TaskQueueLock.h" +#include "KingSystem/Utils/Thread/TaskQueueBase.h" + +namespace ksys::util { + +TaskQueueLock::TaskQueueLock() = default; + +TaskQueueLock::~TaskQueueLock() { + unlock(); +} + +void TaskQueueLock::lock(TaskQueueBase* queue) { + if (!mQueue) { + mQueue = queue; + mQueue->lock(); + } +} + +void TaskQueueLock::unlock() { + if (mQueue) { + mQueue->unlock(); + mQueue = nullptr; + } +} + +} // namespace ksys::util diff --git a/src/KingSystem/Utils/Thread/TaskQueueLock.h b/src/KingSystem/Utils/Thread/TaskQueueLock.h new file mode 100644 index 00000000..2bc44035 --- /dev/null +++ b/src/KingSystem/Utils/Thread/TaskQueueLock.h @@ -0,0 +1,28 @@ +#pragma once + +#include <prim/seadRuntimeTypeInfo.h> +#include "KingSystem/Utils/Thread/TaskThread.h" +#include "KingSystem/Utils/Types.h" + +namespace ksys::util { + +class TaskQueueBase; + +class TaskQueueLock { + SEAD_RTTI_BASE(TaskQueueLock) +public: + TaskQueueLock(); + explicit TaskQueueLock(TaskThread* thread) : TaskQueueLock() { thread->lock(this); } + TaskQueueLock(const TaskQueueLock&) = delete; + virtual ~TaskQueueLock(); + TaskQueueLock& operator=(const TaskQueueLock&) = delete; + + void lock(TaskQueueBase* queue); + void unlock(); + +private: + TaskQueueBase* mQueue = nullptr; +}; +KSYS_CHECK_SIZE_NX150(TaskQueueLock, 0x10); + +} // namespace ksys::util diff --git a/src/KingSystem/Utils/Thread/TaskThread.cpp b/src/KingSystem/Utils/Thread/TaskThread.cpp new file mode 100644 index 00000000..ff2e0f91 --- /dev/null +++ b/src/KingSystem/Utils/Thread/TaskThread.cpp @@ -0,0 +1,261 @@ +#include "KingSystem/Utils/Thread/TaskThread.h" +#include <thread/seadThread.h> +#include "KingSystem/Utils/Thread/Task.h" +#include "KingSystem/Utils/Thread/TaskQueue.h" +#include "KingSystem/Utils/Thread/TaskQueueLock.h" + +namespace ksys::util { + +TaskThread::TaskThread(const sead::SafeString& name, sead::Heap* heap, s32 priority, + sead::MessageQueue::BlockType block_type, + sead::MessageQueue::Element quit_msg, s32 stack_size, s32 message_queue_size) + : Thread(name, heap, priority, block_type, quit_msg, stack_size, message_queue_size), + mPauseResumeEvent(heap), mTaskProcessedEvent(heap) {} + +TaskThread::~TaskThread() { + if (!mTaskQueue) + return; + + mTaskQueue->removeThread(this); + if (mFlags.isOff(Flag::DoesNotOwnTaskQueue) && mTaskQueue) { + delete mTaskQueue; + mTaskQueue = nullptr; + } +} + +bool TaskThread::init(const TaskThread::InitArg& arg) { + if (arg.queue) { + mTaskQueue = arg.queue; + mFlags.set(Flag::DoesNotOwnTaskQueue); + } else { + mTaskQueue = new (arg.heap) TaskQueue(arg.heap); + TaskQueueBase::InitArg queue_arg; + queue_arg.enable_locks = false; + queue_arg.task_selection_delegate = nullptr; + queue_arg.heap = arg.heap; + queue_arg.num_lanes = arg.num_lanes; + queue_arg.max_num_threads = 1; + mTaskQueue->init(queue_arg); + } + + mTaskQueue->addThread(this); + + mPauseResumeEvent.initialize(true); + mPauseResumeEvent.setSignal(); + + mTaskProcessedEvent.initialize(true); + mTaskProcessedEvent.setSignal(); + + mBatchSize = arg.batch_size; + + return true; +} + +s32 TaskThread::getNumActiveTasks() const { + return mTaskQueue->getNumActiveTasks(); +} + +void TaskThread::waitForQueueToEmpty() { + mTaskQueue->waitForQueueToEmpty(); +} + +void TaskThread::cancelTasks(u8 id) { + mTaskQueue->cancelTasks(id); +} + +void TaskThread::clearQueue() { + mTaskQueue->clear(); +} + +void TaskThread::lock(TaskQueueLock* lock) { + mTaskQueue->lock(lock); +} + +bool TaskThread::isActiveAndReceivedQueueUpdateMsg() const { + if (mFlags.isOn(Flag::Paused)) + return false; + if (mFlags.isOn(Flag::IsActive)) + return true; + return receivedQueueUpdatedMsg(); +} + +bool TaskThread::isPaused() const { + return mFlags.isOn(Flag::Paused); +} + +bool TaskThread::receivedQueueUpdatedMsg() const { + return mMessageQueue.peek(sead::MessageQueue::BlockType::NonBlocking) == cMessage_QueueUpdated; +} + +void TaskThread::pause() { + if (sead::ThreadMgr::instance()->getCurrentThread() == this) + return; + + if (!mPauseResumeMsg.compareExchange(cMessage_Resume, cMessage_Pause)) + return; + + mPauseResumeEvent.wait(); + mPauseResumeEvent.resetSignal(); + mMessageQueue.jam(cMessage_Pause, sead::MessageQueue::BlockType::Blocking); +} + +void TaskThread::pauseAndWaitForAck() { + if (sead::ThreadMgr::instance()->getCurrentThread() == this) + return; + + pause(); + mPauseResumeEvent.wait(); +} + +void TaskThread::resume() { + if (sead::ThreadMgr::instance()->getCurrentThread() == this) + return; + + if (!mPauseResumeMsg.compareExchange(cMessage_Pause, cMessage_Resume)) + return; + + mPauseResumeEvent.wait(); + mPauseResumeEvent.resetSignal(); + mMessageQueue.jam(cMessage_Resume, sead::MessageQueue::BlockType::Blocking); +} + +void TaskThread::resumeAndWaitForAck() { + if (sead::ThreadMgr::instance()->getCurrentThread() == this) + return; + + resume(); + mPauseResumeEvent.wait(); +} + +bool TaskThread::isBusyProcessingTask() const { + return mFlags.isOn(Flag::IsBusyProcessingTask); +} + +bool TaskThread::isLookingForTask() const { + return mFlags.isOn(Flag::IsLookingForTask); +} + +// NON_MATCHING: branching for `if (mTaskQueue->getNumActiveTasks() == 0)`: +// Clang got rid of the branch and merged the two mFlags writes +void TaskThread::calc_(sead::MessageQueue::Element msg) { + if (mFlags.isOn(Flag::Paused)) { + if (msg != cMessage_Resume) + return; + mFlags.reset(Flag::Paused); + mPauseResumeEvent.setSignal(); + } + + if (msg == cMessage_Pause) { + mPauseResumeEvent.setSignal(); + mFlags.set(Flag::Paused); + return; + } + + if (mBatchSize >= 1) + mNumRemainingTasksInBatch = mBatchSize; + + while (true) { + { + TaskQueueLock lock{this}; + const auto latest_msg = mMessageQueue.peek(sead::MessageQueue::BlockType::NonBlocking); + + if (latest_msg == mQuitMsg) { + mFlags.reset(Flag::IsActive); + mFlags.reset(Flag::IsLookingForTask); + break; + } + + if (latest_msg == cMessage_Pause) { + mFlags.reset(Flag::IsActive); + mFlags.reset(Flag::IsLookingForTask); + break; + } + + mFlags.set(Flag::IsActive); + + mFlags.set(Flag::IsLookingForTask); + mTaskQueue->fetchTask(&mTask); + mFlags.reset(Flag::IsLookingForTask); + + if (mTask == nullptr) { + mFlags.reset(Flag::IsActive); + mFlags.reset(Flag::IsLookingForTask); + mTaskQueue->signalEmptyEventsIfNeeded(); + break; + } + + mFlags.reset(Flag::IsLookingForTask); + + mFlags.set(Flag::IsBusyProcessingTask); + mTask->setThread(this); + } + + mTask->run(); + + Task* task; + { + TaskQueueLock lock{this}; + mTask->onRunFinished(); + task = mTask; + mTask = nullptr; + } + + if (task) { + TaskPostRunResult result; + task->invokePostRunCallback(&result); + + TaskQueueLock lock{this}; + if (!result.getResult()) + task->finish(); + + mFlags.reset(Flag::IsBusyProcessingTask); + mTaskProcessedEvent.setSignal(); + mTaskQueue->signalEmptyEventsIfNeeded(); + + if (mTaskQueue->getNumActiveTasks() == 0) { + mFlags.reset(Flag::IsActive); +#ifdef MATCHING_HACK_NX_CLANG + // To make it easier to see what this function is functionally equivalent. + // Does not fix the matching issue, but turns it into a 2-line reordering. + asm("" ::: "memory"); +#endif + break; + } + + mFlags.set(Flag::IsLookingForTask); + } + + if (mBatchSize <= 0) + continue; + + --mNumRemainingTasksInBatch; + if (mNumRemainingTasksInBatch == 0) { + sead::Thread::yield(); + mNumRemainingTasksInBatch = mBatchSize; + } + } + + mFlags.reset(Flag::IsActive); + mFlags.reset(Flag::IsLookingForTask); +} + +bool TaskThread::receivedPauseMsg() const { + return mMessageQueue.peek(sead::MessageQueue::BlockType::NonBlocking) == cMessage_Pause; +} + +bool TaskThread::receivedResumeMsg() const { + return mMessageQueue.peek(sead::MessageQueue::BlockType::NonBlocking) == cMessage_Resume; +} + +bool TaskThread::receivedQuitMsg() const { + const auto msg = static_cast<s32>(mQuitMsg); + return mMessageQueue.peek(sead::MessageQueue::BlockType::NonBlocking) == msg; +} + +void TaskThread::cancelCurrentTask() { + TaskQueueLock lock{this}; + if (mTask) + mTask->cancel(); +} + +} // namespace ksys::util diff --git a/src/KingSystem/Utils/Thread/TaskThread.h b/src/KingSystem/Utils/Thread/TaskThread.h new file mode 100644 index 00000000..8d2cb559 --- /dev/null +++ b/src/KingSystem/Utils/Thread/TaskThread.h @@ -0,0 +1,109 @@ +#pragma once + +#include <prim/seadRuntimeTypeInfo.h> +#include <prim/seadTypedBitFlag.h> +#include <thread/seadAtomic.h> +#include <thread/seadThread.h> +#include "KingSystem/Utils/Thread/Event.h" +#include "KingSystem/Utils/Types.h" + +namespace ksys::util { + +class Task; +class TaskQueueBase; +class TaskQueueLock; + +class TaskThread : public sead::Thread { + SEAD_RTTI_BASE(TaskThread) +public: + enum Message { + cMessage_QueueUpdated = 1, + cMessage_Pause = 2, + cMessage_Resume = 3, + }; + + struct InitArg { + /// Number of lanes if a new queue is to be created. + /// Only used if queue is nullptr. + u16 num_lanes; + /// Number of tasks to process in a row before yielding. Can be zero to disable the limit. + u32 batch_size; + /// Heap that will be used to allocate a new queue if necessary. + /// Only used if queue is nullptr. + sead::Heap* heap; + /// Task queue. If null, a new queue will be created and owned by this thread. + TaskQueueBase* queue; + }; + KSYS_CHECK_SIZE_NX150(InitArg, 0x18); + + TaskThread(const sead::SafeString& name, sead::Heap* heap, s32 priority, + sead::MessageQueue::BlockType block_type, sead::MessageQueue::Element quit_msg, + s32 stack_size, s32 message_queue_size); + ~TaskThread() override; + + bool init(const InitArg& arg); + + s32 getNumActiveTasks() const; + + void waitForQueueToEmpty(); + void cancelTasks(u8 id); + void clearQueue(); + void lock(TaskQueueLock* lock); + + bool isActiveAndReceivedQueueUpdateMsg() const; + bool isPaused() const; + bool receivedQueueUpdatedMsg() const; + + void pause(); + void pauseAndWaitForAck(); + void resume(); + void resumeAndWaitForAck(); + + bool isBusyProcessingTask() const; + bool isLookingForTask() const; + + bool receivedPauseMsg() const; + bool receivedResumeMsg() const; + bool receivedQuitMsg() const; + + void cancelCurrentTask(); + + TaskQueueBase* getTaskQueue() const { return mTaskQueue; } + +protected: + friend class TaskQueueBase; + + enum class Flag { + _1 = 0x1, + _2 = 0x2, + _4 = 0x4, + Paused = 0x8, + /// The task queue is *not* owned by this TaskThread. + DoesNotOwnTaskQueue = 0x10, + /// A batch is being processed. + IsActive = 0x20, + /// This thread is looking for a task to process. + IsLookingForTask = 0x40, + /// A task is being processed. + IsBusyProcessingTask = 0x80, + }; + + void calc_(sead::MessageQueue::Element msg) override; + + sead::TypedBitFlag<Flag, u8> mFlags = [] { + decltype(mFlags) flags; + flags.set(Flag::_2); + flags.set(Flag::_4); + return flags; + }(); + Task* mTask = nullptr; + s32 mBatchSize = 0; + s32 mNumRemainingTasksInBatch = 0; + sead::Atomic<Message> mPauseResumeMsg = cMessage_Resume; + Event mPauseResumeEvent; + Event mTaskProcessedEvent; + TaskQueueBase* mTaskQueue = nullptr; +}; +KSYS_CHECK_SIZE_NX150(TaskThread, 0x1a0); + +} // namespace ksys::util |
