I am just wondering aloud here, but it seems to me that the core issue that thomas wants to resolve is the reasonably common situation of asking the GPU to compute something but not needing the result ASAP, but later might be next frame, or many frames away.
One idea is the following, is to introduce the idea of "tasks". A task is just a collection of commands to send to GL to get processed. A task can be dependent on other tasks, i.e. the dependent tasks need to be done before the task itself. Lastly, one may want the ability to cancel a task as well (for example new data comes, and if the task is still waiting, then go ahead and cancel it). With that idea clear, here is one way to express that idea:
/* Generate task objects \param n number of tasks objects to create \param tasks specifies an array in which to store the task object ID's */ GenTasks(sizei n, uint *tasks) /* Delete task objects \param n number of tasks objects to delete \param tasks specifies an array of task objects to delete */ DeleteTasks(sizei n, uint *tasks) /* Specify that all subsequent commands are to be for a task to saved for later execution \param task task object to make active. */ BeginTask(uint task); /* Specifies resume of normal GL operation, i.e. commands are no longer added to a task. */ EndTask() /* Specifies that a target task, target_task, may only be executed after a set of other tasks are completed */ AddTaskDependecy(uint target_task, sizei n, uint *tasks); /* Specifies that a target task, target_task, no longer needs to wait for other tasks to complete before being executed */ RemoveTaskDepenency(uint target_task, sizei n, uint *tasks); /* Place a task to be executed, return immediately. An implementation will select when it is executed. */ ExecuteTaskNonBlock(uint task); /* Forces a GL implementation to finish the named task before executing any subsequent GL commands. Does not block. The task must have had ExecuteTaskNonBlock already called. */ TaskBarrier(uint task); /* If a task has not yet completed, but ExecuteTaskNonBlock has been called, cancel the task. The return code is one of -PARTIALLY_EXECUTED task was only partially run -NOT_EXECUTED task never had a chance to run at all -COMPLETED task finished it's run anyways */ enum CancelTask(uint task) /* Query a task \param task task to query \param prop what property to query, values may be: TASK_STATUS NUMBER_DEPENDENT_TASKS, DEPENDENT_TASKS, others? \param out location which to write values TASK_STATUS gives one of: - INACTIVE: task is either complete or not queued - ACTIVE: task is either running right now or queued to be run */ QueryTask(uint task, enum prop, uint *out);
Naturally this opens up lots of messy cans of worms like what happens when a task is cancelled but it is relied upon by another running task, or even if task should be cancellable. Moreover, weather or not the same task can be queued up more than once. Along those lines, weather or not a task is modifiable while it is queued up. Going further state of stuff on which the task resides. These are my preferences for each of these worms:
- A given task may not be queued up more than once, i.e. calling ExecuteTaskNonBlock on a task that has not yet finished but had ExecuteTaskNonBlock called on it before is either an error or ignored. Having the same task queued repeatedly opens up a mess to specify with respect to dependent tasks, i.e. should they get rerun too, etc. This is messy regardless once dependent tasks come into play, I am on the side that calling ExecuteTaskNonBlock on a task already queued or running is ignored
- A task is unchangeable once TaskEnd() is called.
- If a task T relies on any GL objects, such as buffer objects and textures, it uses the state of those objects when it is run. Thus creating a task that uses a buffer object B the values used by the task in it's run are not the values of B when the task was defined; rather they are whatever values B has when the task is run. To get well defined results an application needs to not write to B (except in a task on which T depends) until T completes.
- If a task T is cancelled where T has dependent tasks, then those dependent tasks that were made to run so that T may ran are also cancelled.
- If a task T depends on a task S and S is cancelled where as T is not cancelled, then I don't know. Error? Ignored?
- Circular task dependency is an error.
- Order of operations of tasks is only guaranteed on dependency graph, i.e. the only task guaranteed to be done before a task T are those tasks that T depends on.
More worms are the nature of GL state, I would want that the GL state be per task, thus GL state changes are isolated to the task and each task has their own GL state vector.
This sounds like the D3D's deferred command stuff.
This suggestion forces the developer to say exactly how important something is by specifying there dependency task. An ideal implementation of the above would interpret TaskBarrier() as moving the task and it's dependency up to most important thing to get done. Not too sure if there should be a blocking version of TaskBarrier as well though.....