Task graph
Description
While not entirely necessary, we’re going to use TaskGraph, which allows us to compile a list of GPU tasks and their dependencies into a synchronized set of commands. This simplifies your code by making different tasks completely self-contained, while also generating the most optimal synchronization for the tasks you describe. To use TaskGraph, as its also an optional feature, add the include path <daxa/utils/task_graph.hpp>
at the top of our main file.
Creating a vertex uploading task
Before we can use a task graph, we first need to create actual tasks that can be executed. The first task we are going to create will upload vertex data to the GPU.
Each task struct must consist of a child struct ‘Uses’ that will store all shared resources, as well as a callback function that gets called whenever the task is executed.
For our task, this base task structure will look like this:
In the task
callback function, for the sake of brevity, we will create the data we will upload. In this sample, we will use the standard triangle vertices.
To send the data to the GPU, we can create a staging buffer, which has host access, so that we can then issue a command to copy from this buffer to the dedicated GPU memory.
We can also ask the command recorder to destroy this temporary buffer since we don’t care about it living, but we DO need it to survive through its usage on the GPU (which won’t happen until after these commands are submitted), so we tell the command recorder to destroy it in a deferred fashion.
We then get the memory-mapped pointer of the staging buffer, and write the data directly to it.
Creating a Rendering task
We will again create a simple task:
We first need to get the screen width and height in the callback function. We can do this by getting the target image dimensions.
Next, we need to record an actual renderpass. The values are pretty self-explanatory if you have used OpenGL before. This contains the actual rendering logic.
Creating a Rendering TaskGraph
When using TaskGraph, we must create “virtual” resources (we call them task resources) whose usages are tracked, allowing for correct synchronization for them.
Back in our main method, the first we’ll make is the swap chain image task resource. We could immediately give this task image an image ID. But in the case of the swapchain images we need to reacquire a new image every frame.
We will also create a buffer task resource, for our MyVertex buffer buffer_id. We do something a little special here, which is that we set the initial access of the buffer to be vertex shader read, and that’s because we’ll create a task list that will upload the buffer.
Next, we need to create the actual task graph itself:
We need to explicitly declare all uses of persistent task resources because manually marking used resources makes it possible to detect errors in your graph recording.
Since we need the task graph to do something, we add the task that draws to the screen:
Once we have added all the tasks we want, we have to tell the task graph we are done.
We have now created a new task graph that can simply repeat the steps it was given,
Creating a vertex uploading TaskGraph
Now we record a secondary task graph, that is only executed once (in our sample code). This task graph uploads data to the ‘vertex buffer’. Task Graph resources automatically link between graphics at runtime, so you don’t need to be concerned about the synchronization of the vertex buffer between the two graphs.
Because this is only executed once, we can define it in a separate context.