diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 99450c50..c310b7bf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -52,6 +52,7 @@ jobs: if not exist ../out/pl_image_ext.dll exit 1 if not exist ../out/pl_stats_ext.dll exit 1 if not exist ../out/pl_debug_ext.dll exit 1 + if not exist ../out/pl_job_ext.dll exit 1 if not exist ../out/pl_gpu_allocators_ext.dll exit 1 if not exist ../out/pl_resource_ext.dll exit 1 if not exist ../out/pl_ecs_ext.dll exit 1 @@ -103,6 +104,7 @@ jobs: test -f ./out/pl_image_ext.dylib || exit 1 test -f ./out/pl_debug_ext.dylib || exit 1 test -f ./out/pl_graphics_ext.dylib || exit 1 + test -f ./out/pl_job_ext.dylib || exit 1 test -f ./out/pl_gpu_allocators_ext.dylib || exit 1 test -f ./out/pl_resource_ext.dylib || exit 1 test -f ./out/pl_ecs_ext.dylib || exit 1 @@ -167,6 +169,7 @@ jobs: test -f ./out/pl_image_ext.so || exit 1 test -f ./out/pl_stats_ext.so || exit 1 test -f ./out/pl_debug_ext.so || exit 1 + test -f ./out/pl_job_ext.so || exit 1 test -f ./out/pl_gpu_allocators_ext.so || exit 1 test -f ./out/pl_resource_ext.so || exit 1 test -f ./out/pl_ecs_ext.so || exit 1 diff --git a/apps/app.c b/apps/app.c index ce58fcb2..69081dac 100644 --- a/apps/app.c +++ b/apps/app.c @@ -35,6 +35,7 @@ Index of this file: #include "pl_ecs_ext.h" #include "pl_resource_ext.h" #include "pl_ref_renderer_ext.h" +#include "pl_job_ext.h" // misc #include "helper_windows.h" @@ -105,6 +106,7 @@ const plEcsI* gptEcs = NULL; const plCameraI* gptCamera = NULL; const plResourceI* gptResource = NULL; const plRefRendererI* gptRenderer = NULL; +const plJobI* gptJobs = NULL; //----------------------------------------------------------------------------- // [SECTION] pl_app_load @@ -137,6 +139,7 @@ pl_app_load(plApiRegistryI* ptApiRegistry, plAppData* ptAppData) gptCamera = ptApiRegistry->first(PL_API_CAMERA); gptResource = ptApiRegistry->first(PL_API_RESOURCE); gptRenderer = ptApiRegistry->first(PL_API_REF_RENDERER); + gptJobs = ptApiRegistry->first(PL_API_JOB); return ptAppData; } @@ -160,6 +163,7 @@ pl_app_load(plApiRegistryI* ptApiRegistry, plAppData* ptAppData) // load extensions const plExtensionRegistryI* ptExtensionRegistry = ptApiRegistry->first(PL_API_EXTENSION_REGISTRY); ptExtensionRegistry->load("pl_image_ext", NULL, NULL, false); + ptExtensionRegistry->load("pl_job_ext", NULL, NULL, false); ptExtensionRegistry->load("pl_stats_ext", NULL, NULL, false); ptExtensionRegistry->load("pl_graphics_ext", NULL, NULL, false); ptExtensionRegistry->load("pl_gpu_allocators_ext", NULL, NULL, false); @@ -167,7 +171,7 @@ pl_app_load(plApiRegistryI* ptApiRegistry, plAppData* ptAppData) ptExtensionRegistry->load("pl_ecs_ext", NULL, NULL, false); ptExtensionRegistry->load("pl_resource_ext", NULL, NULL, false); ptExtensionRegistry->load("pl_ref_renderer_ext", NULL, NULL, true); - + // load apis gptWindows = ptApiRegistry->first(PL_API_WINDOW); gptThreads = ptApiRegistry->first(PL_API_THREADS); @@ -182,6 +186,7 @@ pl_app_load(plApiRegistryI* ptApiRegistry, plAppData* ptAppData) gptCamera = ptApiRegistry->first(PL_API_CAMERA); gptResource = ptApiRegistry->first(PL_API_RESOURCE); gptRenderer = ptApiRegistry->first(PL_API_REF_RENDERER); + gptJobs = ptApiRegistry->first(PL_API_JOB); const plWindowDesc tWindowDesc = { .pcName = "Pilot Light Example", @@ -422,7 +427,7 @@ pl_app_update(plAppData* ptAppData) .bShowAllBoundingBoxes = ptAppData->bDrawAllBoundingBoxes, .bShowVisibleBoundingBoxes = ptAppData->bDrawVisibleBoundingBoxes, .bShowOrigin = false, - .bCullStats = true, + .bCullStats = false, .ptViewCamera = ptCamera2, .ptCullCamera = ptCamera2 }; diff --git a/extensions/pl_job_ext.c b/extensions/pl_job_ext.c new file mode 100644 index 00000000..13f1eff1 --- /dev/null +++ b/extensions/pl_job_ext.c @@ -0,0 +1,345 @@ +/* + pl_job_ext.c +*/ + +/* +Index of this file: +// [SECTION] includes +// [SECTION] internal structs +// [SECTION] global data +// [SECTION] free list functions +// [SECTION] implementation +// [SECTION] public api implementation +// [SECTION] extension loading +// [SECTION] unity build +*/ + +//----------------------------------------------------------------------------- +// [SECTION] includes +//----------------------------------------------------------------------------- + +#include "pilotlight.h" +#include "pl_job_ext.h" +#include "pl_os.h" + +//----------------------------------------------------------------------------- +// [SECTION] internal structs +//----------------------------------------------------------------------------- + +typedef struct _plAtomicCounterNode +{ + plAtomicCounter* ptCounter; + uint32_t uNodeIndex; + uint32_t uNextNode; +} plAtomicCounterNode; + +typedef struct _plSubmittedJob +{ + plJobDesc tJob; + plAtomicCounter* ptCounter; + uint32_t uNodeIndex; +} plSubmittedJob; + +typedef struct _plJobManagerData +{ + bool bRunning; + uint32_t uThreadCount; + plThread* aptThreads[PL_MAX_JOB_THREADS]; + + // counter free list data + plAtomicCounterNode atNodes[PL_MAX_JOBS]; + uint32_t uFreeList; + + // queue data + plConditionVariable* ptConditionVariable; + plCriticalSection* ptCriticalSection; + plAtomicCounter* ptQueueLatch; // 1 - locked, 0 - unlocked + uint32_t uFrontIndex; + uint32_t uBackIndex; + uint32_t uJobCount; + plSubmittedJob atJobs[PL_MAX_JOBS]; // ring buffer +} plJobManagerData; + +//----------------------------------------------------------------------------- +// [SECTION] global data +//----------------------------------------------------------------------------- + +static const plThreadsI* gptThreads = NULL; +static const plAtomicsI* gptAtomics = NULL; + +static plJobManagerData gptData = {0}; + +//----------------------------------------------------------------------------- +// [SECTION] free list functions +//----------------------------------------------------------------------------- + +static void +pl__add_node_to_freelist(uint32_t uNode) +{ + gptData.atNodes[uNode].uNextNode = gptData.uFreeList; + gptData.uFreeList = uNode; +} + +static void +pl__remove_node_from_freelist(uint32_t uNode) +{ + + bool bFound = false; + if(gptData.uFreeList == uNode) + { + gptData.uFreeList = gptData.atNodes[uNode].uNextNode; + bFound = true; + } + else + { + uint32_t uNextNode = gptData.uFreeList; + while(uNextNode != UINT32_MAX) + { + uint32_t uPrevNode = uNextNode; + uNextNode = gptData.atNodes[uPrevNode].uNextNode; + + if(uNextNode == uNode) + { + gptData.atNodes[uPrevNode].uNextNode = gptData.atNodes[uNode].uNextNode; + bFound = true; + break; + } + } + } + + plAtomicCounterNode* ptNode = &gptData.atNodes[uNode]; + ptNode->uNextNode = UINT32_MAX; + PL_ASSERT(bFound && "could not find node to remove"); +} + +//----------------------------------------------------------------------------- +// [SECTION] implementation +//----------------------------------------------------------------------------- + +static void +pl__push_jobs_into_queue(plJobDesc* ptJobs, uint32_t uJobCount, plAtomicCounter** pptCounter) +{ + + // try to unlock (spin lock) + while(true) + { + if(gptAtomics->atomic_compare_exchange(gptData.ptQueueLatch, 0, 1)) + { + + // get free atomic counter node + uint32_t uNode = gptData.uFreeList; + pl__remove_node_from_freelist(uNode); + *pptCounter = gptData.atNodes[uNode].ptCounter; + + // store job count into counter + gptAtomics->atomic_store(*pptCounter, (uint64_t)uJobCount); + + // set total job count in queue + gptData.uJobCount += uJobCount; + PL_ASSERT(gptData.uJobCount < PL_MAX_JOBS); + + // push jobs into queue + for(uint32_t i = 0; i < uJobCount; i++) + { + gptData.atJobs[gptData.uBackIndex].tJob = ptJobs[i]; + gptData.atJobs[gptData.uBackIndex].ptCounter = *pptCounter; + gptData.atJobs[gptData.uBackIndex].uNodeIndex = uNode; + gptData.uBackIndex--; + if(gptData.uBackIndex == UINT32_MAX) // wrap around + gptData.uBackIndex = PL_MAX_JOBS - 1; + } + break; + } + } + + // unlock + gptAtomics->atomic_store(gptData.ptQueueLatch, 0); + + // wake any sleeping threads + gptThreads->wake_all_condition_variable(gptData.ptConditionVariable); +} + +static bool +pl__pop_job_off_queue(plSubmittedJob* ptJobOut) +{ + bool bHasJob = false; + + // try to unlock (spin lock) + while(true) + { + if(gptAtomics->atomic_compare_exchange(gptData.ptQueueLatch, 0, 1)) + { + if(gptData.uJobCount != 0) + { + *ptJobOut = gptData.atJobs[gptData.uFrontIndex--]; + if(gptData.uFrontIndex == UINT32_MAX) // wrap + gptData.uFrontIndex = PL_MAX_JOBS - 1; + + // update total job count + gptData.uJobCount--; + bHasJob = true; + } + break; + } + } + + // unlock + gptAtomics->atomic_store(gptData.ptQueueLatch, 0); + + return bHasJob; +} + +static void +pl__wait_for_counter(plAtomicCounter* ptCounter, uint32_t uValue) +{ + // wait for counter to reach value (or less) + while(true) + { + const int64_t ilLoadedValue = gptAtomics->atomic_load(ptCounter); + if(ilLoadedValue <= (int64_t)uValue) + break; + } + + // try to unlock (spin lock) + while(true) + { + if(gptAtomics->atomic_compare_exchange(gptData.ptQueueLatch, 0, 1)) + { + // find counter index & return to free list + bool bFound = false; + for(uint32_t i = 0; i < PL_MAX_JOBS; i++) + { + if(gptData.atNodes[i].ptCounter == ptCounter) + { + pl__add_node_to_freelist(i); + bFound = true; + break; + } + } + PL_ASSERT(bFound); + break; + } + } + + // unlock + gptAtomics->atomic_store(gptData.ptQueueLatch, 0); +} + +static void* +pl__thread_procedure(void* pData) +{ + // check for available job + plSubmittedJob tJob = {0}; + while(gptData.bRunning) + { + + if(pl__pop_job_off_queue(&tJob)) + { + // run task + tJob.tJob.task(tJob.tJob.pData); + + // decrement atomic counter + gptAtomics->atomic_decrement(tJob.ptCounter); + + // reset job + tJob.tJob.task = NULL; + tJob.tJob.pData = NULL; + tJob.ptCounter = NULL; + } + else // no jobs + { + // sleep thread based on conditional variable (to be awaken once new jobs are pushed onto queue) + gptThreads->enter_critical_section(gptData.ptCriticalSection); + gptThreads->sleep_condition_variable(gptData.ptConditionVariable, gptData.ptCriticalSection); + gptThreads->leave_critical_section(gptData.ptCriticalSection); + } + } + return NULL; +} + +static void +pl__initialize(uint32_t uThreadCount) +{ + + if(uThreadCount == 0) + uThreadCount = gptThreads->get_hardware_thread_count() - 1; + + PL_ASSERT(uThreadCount < PL_MAX_JOB_THREADS); + gptData.bRunning = true; + gptData.uThreadCount = uThreadCount; + gptAtomics->create_atomic_counter(0, &gptData.ptQueueLatch); + gptThreads->create_condition_variable(&gptData.ptConditionVariable); + gptThreads->create_critical_section(&gptData.ptCriticalSection); + + for(uint32_t i = 0; i < PL_MAX_JOBS; i++) + { + gptData.atJobs[i].tJob.task = NULL; + gptData.atJobs[i].tJob.pData = NULL; + gptAtomics->create_atomic_counter(0, &gptData.atNodes[i].ptCounter); + gptData.atNodes[i].uNodeIndex = i; + gptData.atNodes[i].uNextNode = i + 1; + } + gptData.uFreeList = 0; + + for(uint32_t i = 0; i < uThreadCount; i++) + gptThreads->create_thread(pl__thread_procedure, NULL, &gptData.aptThreads[i]); +} + +static void +pl__cleanup(void) +{ + gptData.bRunning = false; + gptThreads->wake_all_condition_variable(gptData.ptConditionVariable); + for(uint32_t i = 0; i < gptData.uThreadCount; i++) + gptThreads->join_thread(gptData.aptThreads[i]); + + gptAtomics->destroy_atomic_counter(&gptData.ptQueueLatch); + gptThreads->destroy_condition_variable(&gptData.ptConditionVariable); + gptThreads->destroy_critical_section(&gptData.ptCriticalSection); + + for(uint32_t i = 0; i < PL_MAX_JOBS; i++) + { + gptData.atJobs[i].tJob.task = NULL; + gptData.atJobs[i].tJob.pData = NULL; + gptAtomics->destroy_atomic_counter(&gptData.atNodes[i].ptCounter); + } +} + +//----------------------------------------------------------------------------- +// [SECTION] public api implementation +//----------------------------------------------------------------------------- + +const plJobI* +pl_load_job_api(void) +{ + static const plJobI tApi = { + .initialize = pl__initialize, + .cleanup = pl__cleanup, + .wait_for_counter = pl__wait_for_counter, + .run_jobs = pl__push_jobs_into_queue + }; + return &tApi; +} + +//----------------------------------------------------------------------------- +// [SECTION] extension loading +//----------------------------------------------------------------------------- + +PL_EXPORT void +pl_load_ext(plApiRegistryI* ptApiRegistry, bool bReload) +{ + const plDataRegistryI* ptDataRegistry = ptApiRegistry->first(PL_API_DATA_REGISTRY); + pl_set_memory_context(ptDataRegistry->get_data(PL_CONTEXT_MEMORY)); + gptThreads = ptApiRegistry->first(PL_API_THREADS); + gptAtomics = ptApiRegistry->first(PL_API_ATOMICS); + if(bReload) + ptApiRegistry->replace(ptApiRegistry->first(PL_API_JOB), pl_load_job_api()); + else + ptApiRegistry->add(PL_API_JOB, pl_load_job_api()); +} + +PL_EXPORT void +pl_unload_ext(plApiRegistryI* ptApiRegistry) +{ + +} diff --git a/extensions/pl_job_ext.h b/extensions/pl_job_ext.h new file mode 100644 index 00000000..19516292 --- /dev/null +++ b/extensions/pl_job_ext.h @@ -0,0 +1,93 @@ +/* + pl_job_ext.h +*/ + +/* +Index of this file: +// [SECTION] header mess +// [SECTION] defines +// [SECTION] includes +// [SECTION] APIs +// [SECTION] forward declarations +// [SECTION] public api +// [SECTION] public api structs +// [SECTION] structs +*/ + +//----------------------------------------------------------------------------- +// [SECTION] header mess +//----------------------------------------------------------------------------- + +#ifndef PL_JOB_EXT_H +#define PL_JOB_EXT_H + +#define PL_JOB_EXT_VERSION "1.0.0" +#define PL_JOB_EXT_VERSION_NUM 100000 + +//----------------------------------------------------------------------------- +// [SECTION] defines +//----------------------------------------------------------------------------- + +#ifndef PL_MAX_JOBS + #define PL_MAX_JOBS 64 +#endif + +#ifndef PL_MAX_JOB_THREADS + #define PL_MAX_JOB_THREADS 64 +#endif + +//----------------------------------------------------------------------------- +// [SECTION] includes +//----------------------------------------------------------------------------- + +#include + +//----------------------------------------------------------------------------- +// [SECTION] APIs +//----------------------------------------------------------------------------- + +#define PL_API_JOB "PL_API_JOB" +typedef struct _plJobI plJobI; + +//----------------------------------------------------------------------------- +// [SECTION] forward declarations +//----------------------------------------------------------------------------- + +// basic types +typedef struct _plJobDesc plJobDesc; + +// external +typedef struct _plAtomicCounter plAtomicCounter; // pl_os.h + +//----------------------------------------------------------------------------- +// [SECTION] public api +//----------------------------------------------------------------------------- + +const plJobI* pl_load_job_api(void); + +//----------------------------------------------------------------------------- +// [SECTION] public api structs +//----------------------------------------------------------------------------- + +typedef struct _plJobI +{ + // setup/shutdown + void (*initialize)(uint32_t uThreadCount); + void (*cleanup)(void); + + // typical usage + void (*run_jobs) (plJobDesc* ptJobs, uint32_t uJobCount, plAtomicCounter** pptCounter); + void (*wait_for_counter)(plAtomicCounter* ptCounter, uint32_t uValue); +} plJobI; + +//----------------------------------------------------------------------------- +// [SECTION] structs +//----------------------------------------------------------------------------- + +typedef struct _plJobDesc +{ + void (*task)(void* pData); + void* pData; +} plJobDesc; + +#endif // PL_JOB_EXT_H \ No newline at end of file diff --git a/scripts/gen_build.py b/scripts/gen_build.py index 218bc70a..b350e0bb 100644 --- a/scripts/gen_build.py +++ b/scripts/gen_build.py @@ -120,6 +120,7 @@ def add_plugin_to_metal_app(name, reloadable, objc = False, binary_name = None): add_plugin_to_vulkan_app("pl_image_ext", False) add_plugin_to_vulkan_app("pl_vulkan_ext", False, "pl_graphics_ext") add_plugin_to_vulkan_app("pl_stats_ext", False) + add_plugin_to_vulkan_app("pl_job_ext", False) add_plugin_to_vulkan_app("pl_ecs_ext", False) add_plugin_to_vulkan_app("pl_resource_ext", False) add_plugin_to_vulkan_app("pl_gpu_allocators_ext", False) @@ -128,6 +129,7 @@ def add_plugin_to_metal_app(name, reloadable, objc = False, binary_name = None): add_plugin_to_metal_app("pl_debug_ext", False) add_plugin_to_metal_app("pl_image_ext", False) add_plugin_to_metal_app("pl_stats_ext", False) + add_plugin_to_metal_app("pl_job_ext", False) add_plugin_to_metal_app("pl_ecs_ext", False) add_plugin_to_metal_app("pl_resource_ext", False) add_plugin_to_metal_app("pl_metal_ext", False, True, "pl_graphics_ext") diff --git a/scripts/package.py b/scripts/package.py index 5c35f7e3..1384cfb3 100644 --- a/scripts/package.py +++ b/scripts/package.py @@ -19,6 +19,7 @@ "pl_ecs_ext", "pl_stats_ext", "pl_resource_ext", + "pl_job_ext", "pl_ref_renderer_ext", "pl_gpu_allocators_ext", ]