feat: add JobSystem
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
#include <destrum/Scene/ResourceCollector.h>
|
||||
#include <destrum/Scene/Scene.h>
|
||||
#include <destrum/Scene/SceneManager.h>
|
||||
#include <destrum/Threading/JobSystem.h>
|
||||
#include <destrum/Util/DeltaTime.h>
|
||||
|
||||
namespace {
|
||||
@@ -60,6 +61,7 @@ void App::init(const AppParams& params)
|
||||
tracy::SetThreadName("Main Thread");
|
||||
TracySetProgramName(params.appName.c_str());
|
||||
AssetFS::GetInstance().Init(params.exeDir);
|
||||
JobSystem::GetInstance().Init();
|
||||
|
||||
window.Create({
|
||||
.title = params.windowTitle,
|
||||
@@ -171,6 +173,7 @@ void App::run()
|
||||
continue;
|
||||
}
|
||||
|
||||
JobSystem::GetInstance().DrainMainThreadQueue();
|
||||
if (layer) {
|
||||
layer->onBeginFrame();
|
||||
}
|
||||
@@ -386,6 +389,7 @@ void App::cleanup()
|
||||
// The one place we wait for the GPU on shutdown: the cleanup calls below destroy
|
||||
// pipelines and other objects directly, which is only valid once nothing is in flight.
|
||||
attempt([this] { gfxDevice.waitIdle(); });
|
||||
attempt([] { JobSystem::GetInstance().Shutdown(); });
|
||||
|
||||
attempt([this] {
|
||||
if (layer) {
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
#include <destrum/Threading/JobSystem.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <exception>
|
||||
#include <string>
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <tracy/Tracy.hpp>
|
||||
#include <common/TracySystem.hpp>
|
||||
|
||||
JobSystem::~JobSystem()
|
||||
{
|
||||
try {
|
||||
Shutdown();
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
|
||||
void JobSystem::Init(std::uint32_t workerCount)
|
||||
{
|
||||
if (IsRunning()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (workerCount == 0) {
|
||||
const std::uint32_t hardwareThreads = std::thread::hardware_concurrency();
|
||||
workerCount = std::max<std::uint32_t>(1, hardwareThreads > 1 ? hardwareThreads - 1 : 1);
|
||||
}
|
||||
|
||||
m_Stopping = false;
|
||||
m_MaxActiveLowJobs = std::max<std::uint32_t>(1, workerCount / 2);
|
||||
|
||||
m_Workers.reserve(workerCount);
|
||||
for (std::uint32_t i = 0; i < workerCount; ++i) {
|
||||
m_Workers.emplace_back([this, i] { WorkerLoop(i); });
|
||||
}
|
||||
spdlog::info("JobSystem started with {} workers", workerCount);
|
||||
}
|
||||
|
||||
void JobSystem::Shutdown()
|
||||
{
|
||||
if (!IsRunning()) {
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard lock(m_Mutex);
|
||||
m_Stopping = true;
|
||||
}
|
||||
m_WorkAvailable.notify_all();
|
||||
|
||||
for (std::thread& worker : m_Workers) {
|
||||
if (worker.joinable()) {
|
||||
worker.join();
|
||||
}
|
||||
}
|
||||
m_Workers.clear();
|
||||
|
||||
{
|
||||
std::lock_guard lock(m_Mutex);
|
||||
m_HighQueue.clear();
|
||||
m_LowQueue.clear();
|
||||
m_ActiveLowJobs = 0;
|
||||
m_Stopping = false;
|
||||
}
|
||||
{
|
||||
std::lock_guard lock(m_MainThreadMutex);
|
||||
m_MainThreadQueue.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void JobSystem::Execute(JobCounter& counter, Job job, JobPriority priority)
|
||||
{
|
||||
counter.m_Pending.fetch_add(1, std::memory_order_relaxed);
|
||||
Submit([&counter, job = std::move(job)] {
|
||||
try {
|
||||
job();
|
||||
} catch (const std::exception& e) {
|
||||
spdlog::error("Job threw: {}", e.what());
|
||||
} catch (...) {
|
||||
spdlog::error("Job threw an unknown exception");
|
||||
}
|
||||
counter.m_Pending.fetch_sub(1, std::memory_order_release);
|
||||
}, priority);
|
||||
}
|
||||
|
||||
void JobSystem::Dispatch(JobCounter& counter, std::uint32_t count, std::uint32_t groupSize,
|
||||
std::function<void(JobDispatchArgs)> job)
|
||||
{
|
||||
if (count == 0) {
|
||||
return;
|
||||
}
|
||||
groupSize = std::max<std::uint32_t>(1, groupSize);
|
||||
|
||||
auto shared = std::make_shared<std::function<void(JobDispatchArgs)>>(std::move(job));
|
||||
const std::uint32_t groupCount = (count + groupSize - 1) / groupSize;
|
||||
for (std::uint32_t group = 0; group < groupCount; ++group) {
|
||||
Execute(counter, [shared, group, groupSize, count] {
|
||||
const std::uint32_t begin = group * groupSize;
|
||||
const std::uint32_t end = std::min(begin + groupSize, count);
|
||||
for (std::uint32_t i = begin; i < end; ++i) {
|
||||
(*shared)(JobDispatchArgs{.index = i, .groupIndex = group});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void JobSystem::Wait(JobCounter& counter)
|
||||
{
|
||||
ZoneScopedN("JobSystem::Wait");
|
||||
while (!counter.IsDone()) {
|
||||
if (!TryRunHighJob()) {
|
||||
std::this_thread::yield();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void JobSystem::RunOnMainThread(Job job)
|
||||
{
|
||||
std::lock_guard lock(m_MainThreadMutex);
|
||||
m_MainThreadQueue.push_back(std::move(job));
|
||||
}
|
||||
|
||||
void JobSystem::DrainMainThreadQueue()
|
||||
{
|
||||
ZoneScopedN("JobSystem::DrainMainThreadQueue");
|
||||
std::vector<Job> jobs;
|
||||
{
|
||||
std::lock_guard lock(m_MainThreadMutex);
|
||||
jobs.swap(m_MainThreadQueue);
|
||||
}
|
||||
for (Job& job : jobs) {
|
||||
try {
|
||||
job();
|
||||
} catch (const std::exception& e) {
|
||||
spdlog::error("Main thread job threw: {}", e.what());
|
||||
} catch (...) {
|
||||
spdlog::error("Main thread job threw an unknown exception");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void JobSystem::Submit(Job job, JobPriority priority)
|
||||
{
|
||||
if (!IsRunning()) {
|
||||
job();
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard lock(m_Mutex);
|
||||
if (priority == JobPriority::High) {
|
||||
m_HighQueue.push_back(std::move(job));
|
||||
} else {
|
||||
m_LowQueue.push_back(std::move(job));
|
||||
}
|
||||
}
|
||||
m_WorkAvailable.notify_one();
|
||||
}
|
||||
|
||||
bool JobSystem::CanTakeLowLocked() const
|
||||
{
|
||||
return !m_LowQueue.empty() && (m_Stopping || m_ActiveLowJobs < m_MaxActiveLowJobs);
|
||||
}
|
||||
|
||||
bool JobSystem::TryPopLocked(Job& job, bool& isLow)
|
||||
{
|
||||
if (!m_HighQueue.empty()) {
|
||||
job = std::move(m_HighQueue.front());
|
||||
m_HighQueue.pop_front();
|
||||
isLow = false;
|
||||
return true;
|
||||
}
|
||||
if (CanTakeLowLocked()) {
|
||||
job = std::move(m_LowQueue.front());
|
||||
m_LowQueue.pop_front();
|
||||
++m_ActiveLowJobs;
|
||||
isLow = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool JobSystem::TryRunHighJob()
|
||||
{
|
||||
Job job;
|
||||
{
|
||||
std::lock_guard lock(m_Mutex);
|
||||
if (m_HighQueue.empty()) {
|
||||
return false;
|
||||
}
|
||||
job = std::move(m_HighQueue.front());
|
||||
m_HighQueue.pop_front();
|
||||
}
|
||||
job();
|
||||
return true;
|
||||
}
|
||||
|
||||
void JobSystem::WorkerLoop(std::uint32_t index)
|
||||
{
|
||||
const std::string name = "Job Worker " + std::to_string(index);
|
||||
tracy::SetThreadName(name.c_str());
|
||||
|
||||
for (;;) {
|
||||
Job job;
|
||||
bool isLow = false;
|
||||
{
|
||||
std::unique_lock lock(m_Mutex);
|
||||
m_WorkAvailable.wait(lock, [this] {
|
||||
return m_Stopping || !m_HighQueue.empty() || CanTakeLowLocked();
|
||||
});
|
||||
if (!TryPopLocked(job, isLow)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
ZoneScopedN("Job");
|
||||
job();
|
||||
}
|
||||
|
||||
if (isLow) {
|
||||
{
|
||||
std::lock_guard lock(m_Mutex);
|
||||
--m_ActiveLowJobs;
|
||||
}
|
||||
m_WorkAvailable.notify_one();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user