Skip to content

Commit

Permalink
BREAKING C API CHANGE: Added Pre and Post Complete completion actions…
Browse files Browse the repository at this point in the history
… to C interface + improved completion action examples
  • Loading branch information
dougbinks committed Mar 3, 2022
1 parent 4528461 commit 6ffccbd
Show file tree
Hide file tree
Showing 4 changed files with 239 additions and 49 deletions.
65 changes: 54 additions & 11 deletions example/CompletionAction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ static std::atomic<int32_t> gs_CountAsDeleted = {0};
static std::atomic<int32_t> gs_CountBsRun = {0};
static std::atomic<int32_t> gs_CountBsDeleted = {0};


struct CompletionActionDelete : ICompletable
{
Dependency m_Dependency;
Expand All @@ -44,10 +43,10 @@ struct CompletionActionDelete : ICompletable
// the dependency task is complete.
void OnDependenciesComplete( TaskScheduler* pTaskScheduler_, uint32_t threadNum_ )
{
// always call base class OnDependenciesComplete first
// Call base class OnDependenciesComplete BEFORE deleting depedent task or self
ICompletable::OnDependenciesComplete( pTaskScheduler_, threadNum_ );

printf("OnDependenciesComplete called on thread %u\n", threadNum_ );
printf("CompletionActionDelete::OnDependenciesComplete called on thread %u\n", threadNum_ );

// In this example we delete the dependency, which is safe to do as the task
// manager will not dereference it at this point.
Expand All @@ -72,23 +71,56 @@ struct SelfDeletingTaskB : ITaskSet

void ExecuteRange( TaskSetPartition range_, uint32_t threadnum_ ) override
{
(void)range_;
++gs_CountBsRun;
printf("SelfDeletingTaskB on thread %u\n", threadnum_);
if( 0 == range_.start )
{
// whilst would normally loop over range_ doing work here we want to only output info once per task
++gs_CountBsRun;
printf("SelfDeletingTaskB on thread %u with set size %u\n", threadnum_, m_SetSize);
}
}

CompletionActionDelete m_TaskDeleter;
Dependency m_Dependency;
};

struct CompletionActionModifyDependentTaskAndDelete : ICompletable
{
Dependency m_Dependency;

ITaskSet* m_pTaskToModify = nullptr;

// We override OnDependenciesComplete to provide an 'action' which occurs after
// the dependency task is complete.
void OnDependenciesComplete( TaskScheduler* pTaskScheduler_, uint32_t threadNum_ )
{
// Modify following task before calling OnDependenciesComplete
m_pTaskToModify->m_SetSize = 10;

// Call base class OnDependenciesComplete AFTER modifying any depedent task
ICompletable::OnDependenciesComplete( pTaskScheduler_, threadNum_ );

printf("CompletionActionModifyDependentTaskAndDelete::OnDependenciesComplete called on thread %u\n", threadNum_ );

// In this example we delete the dependency, which is safe to do as the task
// manager will not dereference it at this point.
// However the dependency task should have no other dependents,
// This class can have dependencies.
delete m_Dependency.GetDependencyTask(); // also deletes this as member
}
};

struct SelfDeletingTaskA : ITaskSet
{
SelfDeletingTaskA()
{
m_TaskDeleter.SetDependency( m_TaskDeleter.m_Dependency, this );
m_TaskModifyAndDelete.SetDependency( m_TaskModifyAndDelete.m_Dependency, this );
SelfDeletingTaskB* pNextTask = new SelfDeletingTaskB();

// we set the dependency of pNextTask on the task deleter, not on this
pNextTask->SetDependency( pNextTask->m_Dependency, &m_TaskDeleter );
pNextTask->SetDependency( pNextTask->m_Dependency, &m_TaskModifyAndDelete );

// Set the completion actions task to modify to be the following task
m_TaskModifyAndDelete.m_pTaskToModify = pNextTask;
}

~SelfDeletingTaskA()
Expand All @@ -101,16 +133,27 @@ struct SelfDeletingTaskA : ITaskSet
{
(void)range_;
++gs_CountAsRun;
printf("SelfDeletingTaskA on thread %u\n", threadnum_);
printf("SelfDeletingTaskA on thread %u with set size %u\n", threadnum_, m_SetSize);
}

CompletionActionDelete m_TaskDeleter;
CompletionActionModifyDependentTaskAndDelete m_TaskModifyAndDelete;
};

static const int RUNS = 10;
static const int RUNS = 100000;

int main(int argc, const char * argv[])
{
// This examples shows CompletionActions used to modify a following tasks parameters and delete tasks
// Task Graph for this example (with names shortened to fit on screen):
//
// pTaskSetA
// ->pCompletionActionA-Modify-ICompletable::OnDependenciesComplete-Delete
// ->pTaskSetB
// ->pCompletionActionB-ICompletable::OnDependenciesComplete-Delete
//
// Note that pTaskSetB must depend on pCompletionActionA NOT pTaskSetA or it could run at the same time as pCompletionActionA
// so cannot be modified.

g_TS.Initialize();

for( int run = 0; run< RUNS; ++run )
Expand Down
180 changes: 152 additions & 28 deletions example/CompletionAction_c.c
Original file line number Diff line number Diff line change
Expand Up @@ -23,56 +23,180 @@

enkiTaskScheduler* pETS;

struct CompletionArgs
struct CompletionArgs_ModifyTask
{
enkiTaskSet* pTaskB;
uint32_t run;
};

struct CompletionArgs_DeleteTask
{
enkiTaskSet* pTask;
enkiDependency* pDependency; // in this example only 1 or 0 dependencies, but generally could be an array
enkiCompletionAction* pCompletionAction;
uint32_t run; // only required for example output, not needed for a general purpose delete task
};

void CompletionFunction( void* pArgs_, uint32_t threadNum_ )
// In this example all our TaskSet functions share the same args struct, but we could use different one
struct TaskSetArgs
{
enkiTaskSet* pTask;
const char* name;
uint32_t run;
};

void CompletionFunctionPreComplete_ModifyDependentTask( void* pArgs_, uint32_t threadNum_ )
{
struct CompletionArgs_ModifyTask* pCompletionArgs_ModifyTask = pArgs_;
struct enkiParamsTaskSet paramsTaskNext = enkiGetParamsTaskSet( pCompletionArgs_ModifyTask->pTaskB );

printf("CompletionFunctionPreComplete_ModifyDependentTask for run %u running on thread %u\n",
pCompletionArgs_ModifyTask->run, threadNum_ );

// in this function we can modify the parameters of any task which depends on this CompletionFunction
// pre complete functions should not be used to delete the current CompletionAction, for that use PostComplete functions
paramsTaskNext.setSize = 10; // modify the set size of the next task - for example this could be based on output from previous task
enkiSetParamsTaskSet( pCompletionArgs_ModifyTask->pTaskB, paramsTaskNext );

free( pCompletionArgs_ModifyTask );
}


void CompletionFunctionPostComplete_DeleteTask( void* pArgs_, uint32_t threadNum_ )
{
struct CompletionArgs* pCompletionArgs = pArgs_;
struct enkiParamsTaskSet params = enkiGetParamsTaskSet( pCompletionArgs->pTask );
uint32_t* pTaskNum = params.pArgs;
printf("CompletionFunction for task %u running on thread %u\n", *pTaskNum, threadNum_ );
enkiDeleteCompletionAction( pETS, pCompletionArgs->pCompletionAction );
enkiDeleteTaskSet( pETS, pCompletionArgs->pTask );
free( pTaskNum );
free( pCompletionArgs );
struct CompletionArgs_DeleteTask* pCompletionArgs_DeleteTask = pArgs_;

printf("CompletionFunctionPostComplete_DeleteTask for run %u running on thread %u\n",
pCompletionArgs_DeleteTask->run, threadNum_ );

// can free memory in post complete

// note must delete a dependency before you delete the dependency task and the task to run on completion
if( pCompletionArgs_DeleteTask->pDependency )
{
enkiDeleteDependency( pETS, pCompletionArgs_DeleteTask->pDependency );
}

free( enkiGetParamsTaskSet( pCompletionArgs_DeleteTask->pTask ).pArgs );
enkiDeleteTaskSet( pETS, pCompletionArgs_DeleteTask->pTask );

enkiDeleteCompletionAction( pETS, pCompletionArgs_DeleteTask->pCompletionAction );

// safe to free our own args in this example as no other function dereferences them
free( pCompletionArgs_DeleteTask );
}

void TaskSetFunc( uint32_t start_, uint32_t end_, uint32_t threadnum_, void* pArgs_ )
{
(void)start_; (void)end_;
uint32_t* pTaskNum = pArgs_;
printf("Task %u running on thread %u\n", *pTaskNum, threadnum_);
struct TaskSetArgs* pTaskSetArgs = pArgs_;
struct enkiParamsTaskSet paramsTaskNext = enkiGetParamsTaskSet( pTaskSetArgs->pTask );
if( 0 == start_ )
{
// for clarity in this example we only output one printf per taskset func called, but would normally loop from start_ to end_ doing work
printf("Task %s for run %u running on thread %u has set size %u\n", pTaskSetArgs->name, pTaskSetArgs->run, threadnum_, paramsTaskNext.setSize);
}

// A TastSetFunction is not a safe place to free it's own pArgs_ as when the setSize > 1 there may be multiple
// calls to this function with the same pArgs_
}


int main(int argc, const char * argv[])
{
// This examples shows CompletionActions used to modify a following tasks parameters and free allocations
// Task Graph for this example (with names shortened to fit on screen):
//
// pTaskSetA
// ->pCompletionActionA-PreFunc-PostFunc
// ->pTaskSetB
// ->pCompletionActionB-(no PreFunc)-PostFunc
//
// Note that pTaskSetB must depend on pCompletionActionA NOT pTaskSetA or it could run at the same time as pCompletionActionA
// so cannot be modified.

struct enkiTaskSet* pTaskSetA;
struct enkiCompletionAction* pCompletionActionA;
struct enkiTaskSet* pTaskSetB;
struct enkiCompletionAction* pCompletionActionB;
struct TaskSetArgs* pTaskSetArgsA;
struct CompletionArgs_ModifyTask* pCompletionArgsA;
struct enkiParamsCompletionAction paramsCompletionActionA;
struct TaskSetArgs* pTaskSetArgsB;
struct enkiDependency* pDependencyOfTaskSetBOnCompletionActionA;
struct CompletionArgs_DeleteTask* pCompletionArgs_DeleteTaskA;
struct CompletionArgs_DeleteTask* pCompletionArgs_DeleteTaskB;
struct enkiParamsCompletionAction paramsCompletionActionB;
int run;
struct enkiParamsCompletionAction paramsCompletionAction;
uint32_t* pTaskNum;
struct CompletionArgs* pCompletionArgs;

pETS = enkiNewTaskScheduler();
enkiInitTaskScheduler( pETS );

// Here we demonstrate using the completion action to delete the tasks on the fly
for( run=0; run<10; ++run )
{
pCompletionArgs = malloc(sizeof(struct CompletionArgs)); // we will free in CompletionFunction
pCompletionArgs->pTask = enkiCreateTaskSet( pETS, TaskSetFunc );
pTaskNum = malloc(sizeof(uint32_t)); // we will free in CompletionFunction
*pTaskNum = run;
enkiSetArgsTaskSet( pCompletionArgs->pTask, pTaskNum );
pCompletionArgs->pCompletionAction = enkiCreateCompletionAction( pETS, CompletionFunction );
paramsCompletionAction = enkiGetParamsCompletionAction( pCompletionArgs->pCompletionAction );
paramsCompletionAction.pArgs = pCompletionArgs;
paramsCompletionAction.pDependency = enkiGetCompletableFromTaskSet( pCompletionArgs->pTask );
enkiSetParamsCompletionAction( pCompletionArgs->pCompletionAction, paramsCompletionAction );

enkiAddTaskSet( pETS, pCompletionArgs->pTask );
// Create all this runs tasks and completion actions
pTaskSetA = enkiCreateTaskSet( pETS, TaskSetFunc );
pCompletionActionA = enkiCreateCompletionAction( pETS,
CompletionFunctionPreComplete_ModifyDependentTask,
CompletionFunctionPostComplete_DeleteTask );
pTaskSetB = enkiCreateTaskSet( pETS, TaskSetFunc );
pCompletionActionB = enkiCreateCompletionAction( pETS,
NULL,
CompletionFunctionPostComplete_DeleteTask );

// Set args for TaskSetA
pTaskSetArgsA = malloc(sizeof(struct TaskSetArgs));
pTaskSetArgsA->run = run;
pTaskSetArgsA->pTask = pTaskSetA;
pTaskSetArgsA->name = "A";
enkiSetArgsTaskSet( pTaskSetA, pTaskSetArgsA );

// Set args for CompletionActionA, and make dependent on TaskSetA through pDependency
pCompletionArgsA = malloc(sizeof(struct CompletionArgs_ModifyTask));
pCompletionArgsA->pTaskB = pTaskSetB;
pCompletionArgsA->run = run;
pCompletionArgs_DeleteTaskA = malloc(sizeof(struct CompletionArgs_DeleteTask));
pCompletionArgs_DeleteTaskA->pTask = pTaskSetA;
pCompletionArgs_DeleteTaskA->pCompletionAction = pCompletionActionA;
pCompletionArgs_DeleteTaskA->pDependency = NULL;
pCompletionArgs_DeleteTaskA->run = run;

paramsCompletionActionA = enkiGetParamsCompletionAction( pCompletionActionA );
paramsCompletionActionA.pArgsPreComplete = pCompletionArgsA;
paramsCompletionActionA.pArgsPostComplete = pCompletionArgs_DeleteTaskA;
paramsCompletionActionA.pDependency = enkiGetCompletableFromTaskSet( pTaskSetA );
enkiSetParamsCompletionAction( pCompletionActionA, paramsCompletionActionA );


// Set args for TaskSetB
pTaskSetArgsB = malloc(sizeof(struct TaskSetArgs));
pTaskSetArgsB->run = run;
pTaskSetArgsB->pTask = pTaskSetB;
pTaskSetArgsB->name = "B";
enkiSetArgsTaskSet( pTaskSetB, pTaskSetArgsB );

// TaskSetB depends on pCompletionActionA
pDependencyOfTaskSetBOnCompletionActionA = enkiCreateDependency( pETS );
enkiSetDependency( pDependencyOfTaskSetBOnCompletionActionA,
enkiGetCompletableFromCompletionAction( pCompletionActionA ),
enkiGetCompletableFromTaskSet( pTaskSetB ) );

// Set args for CompletionActionB, and make dependent on TaskSetB through pDependency
pCompletionArgs_DeleteTaskB = malloc(sizeof(struct CompletionArgs_DeleteTask));
pCompletionArgs_DeleteTaskB->pTask = pTaskSetB;
pCompletionArgs_DeleteTaskB->pDependency = pDependencyOfTaskSetBOnCompletionActionA;
pCompletionArgs_DeleteTaskB->pCompletionAction = pCompletionActionB;
pCompletionArgs_DeleteTaskB->run = run;

paramsCompletionActionB = enkiGetParamsCompletionAction( pCompletionActionB );
paramsCompletionActionB.pArgsPreComplete = NULL; // pCompletionActionB does not have a PreComplete function
paramsCompletionActionB.pArgsPostComplete = pCompletionArgs_DeleteTaskB;
paramsCompletionActionB.pDependency = enkiGetCompletableFromTaskSet( pTaskSetB );
enkiSetParamsCompletionAction( pCompletionActionB, paramsCompletionActionB );


// To launch all, we only add the first TaskSet
enkiAddTaskSet( pETS, pTaskSetA );
}
enkiWaitForAll( pETS );

Expand Down
33 changes: 25 additions & 8 deletions src/TaskScheduler_c.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -81,16 +81,30 @@ struct enkiPinnedTask : IPinnedTask

struct enkiCompletionAction : ICompletable
{
void OnDependenciesComplete( TaskScheduler* pTaskScheduler_, uint32_t threadNum_ )
void OnDependenciesComplete( TaskScheduler* pTaskScheduler_, uint32_t threadNum_ ) override
{
if( completionFunctionPreComplete )
{
completionFunctionPreComplete( pArgsPreComplete, threadNum_ );
}

// make temporaries for post completion as this task could get deleted after OnDependenciesComplete
enkiCompletionFunction tempCompletionFunctionPostComplete = completionFunctionPostComplete;
void* ptempArgsPostComplete = pArgsPostComplete;

ICompletable::OnDependenciesComplete( pTaskScheduler_, threadNum_ );

completionFunction( pArgs, threadNum_ );
if( tempCompletionFunctionPostComplete )
{
tempCompletionFunctionPostComplete( ptempArgsPostComplete, threadNum_ );
}
}

enkiCompletionFunction completionFunction;
enkiCompletionFunction completionFunctionPreComplete;
enkiCompletionFunction completionFunctionPostComplete;
Dependency dependency;
void* pArgs = NULL;
void* pArgsPreComplete = NULL;
void* pArgsPostComplete = NULL;
};

struct enkiDependency : Dependency {}; // empty struct which we will use for dependencies
Expand Down Expand Up @@ -476,13 +490,14 @@ void enkiSetDependency( enkiDependency* pDependency_, enkiCompletable* pDependen
pTaskToRunOnCompletion_->SetDependency( *pDependency_, pDependencyTask_ );
}

enkiCompletionAction* enkiCreateCompletionAction( enkiTaskScheduler* pETS_, enkiCompletionFunction completionFunc_ )
enkiCompletionAction* enkiCreateCompletionAction( enkiTaskScheduler* pETS_, enkiCompletionFunction completionFunctionPreComplete_, enkiCompletionFunction completionFunctionPostComplete_ )
{
const CustomAllocator& customAllocator = pETS_->GetConfig().customAllocator;
enkiCompletionAction* pCA = (enkiCompletionAction*)customAllocator.alloc(
alignof(enkiCompletionAction), sizeof(enkiCompletionAction), customAllocator.userData, ENKI_FILE_AND_LINE );
new(pCA) enkiCompletionAction();
pCA->completionFunction = completionFunc_;
pCA->completionFunctionPreComplete = completionFunctionPreComplete_;
pCA->completionFunctionPostComplete = completionFunctionPostComplete_;
return pCA;
}

Expand All @@ -497,14 +512,16 @@ void enkiDeleteCompletionAction( enkiTaskScheduler* pETS_, enkiCompletionAction*
enkiParamsCompletionAction enkiGetParamsCompletionAction( enkiCompletionAction* pCompletionAction_ )
{
enkiParamsCompletionAction params;
params.pArgs = pCompletionAction_->pArgs;
params.pArgsPreComplete = pCompletionAction_->pArgsPreComplete;
params.pArgsPostComplete = pCompletionAction_->pArgsPostComplete;
params.pDependency = reinterpret_cast<const enkiCompletable*>(
pCompletionAction_->dependency.GetDependencyTask() );
return params;
}

void enkiSetParamsCompletionAction( enkiCompletionAction* pCompletionAction_, enkiParamsCompletionAction params_ )
{
pCompletionAction_->pArgs = params_.pArgs;
pCompletionAction_->pArgsPreComplete = params_.pArgsPreComplete;
pCompletionAction_->pArgsPostComplete = params_.pArgsPostComplete;
pCompletionAction_->SetDependency( pCompletionAction_->dependency, params_.pDependency );
}
Loading

0 comments on commit 6ffccbd

Please sign in to comment.