Welcome back to the DirectX 12 Programming tutorial series! The previous tutorials was all about getting you up and running with the default DirectX 12 application, understanding the PSO and the Command Lists. We still have some basics to cover before we can move on to the really fun stuff.
Today we will take a closer look at Resources and Buffers. Most of the common tasks you want to do with DirectX 12 involves the use of resources. This can be models, textures, data and so on. These resources needs to be loaded and bound to the graphics pipeline to use them.
To use any of these resources, you need to link it to the graphics pipeline. This is called Resource Binding. In DirectX 12, resources are bound by using Descriptors, Descriptor Tables and the Descriptor Heap.
There will be a lot of new terms today, so let’s just dive right in. The first term you will need to know is resource. As mentioned above, a resource is simply any kind of resources your application will use to display what you want. This can be textures, models (vertex data) etc. All resources are derived from ID3D12Resource.
Ultimately, a resource is simply a memory buffer. The difference is only how you operate with it and how GPU sees it.
In our example, we have a few resources identified by ID3D12Resource; our Vertex Buffer, Index Buffer and Constant Buffer. As you know, shader resources are bound directly to the PSO.
Our vertex data could have been loaded from a file exported from Blender, Maya, 3D Studio Max and any other modeling software, procedurally generated or like in our example, a hardcoded list of vertices:
Once we have the data, we need to create a committed resources. A committed resource is created by calling the function CreateCommittedResource(..) on the Direct3D Device. This function will initiate and create the resource object itself, and a heap big enough to contain all of the data. We create the resource using the default heap type using none of the heap flags, we set the size of the resource so it can contain all of our vertices, set the state to COPY_DEST as this will be a destination for the vertex buffer, and the pointer to the memory where our resource will be, the ID3D12Resource object.
Once we got this, we are ready to copy the resource data to it.
DirectX 12 can map the resource without locking it, so GPU operates with the current version while your code on CPU updates the resource data. Once you call Unmap on a resource, DirectX updates the actual resource on GPU.
A descriptor is simply an object that describes a resource stored somewhere in the memory (like the one we created above), and it’s used to describe this resource to the GPU.
Think of them as a view for the GPU into a set of data, like vertices or textures.
The GPU will need to know what it’s looking at, and how to deal with it. A descriptor does just this, it let’s the GPU know what kind of resources we are dealing with.
In previous versions of DirectX, we explicitly created a particular resource (a texture, a buffer) and set the access flags. In DirectX 12, resource binding is not tracked so it’s your job as the programmer to handle the lifetime of the objects. Descriptors is a part of the process you will need to handle.
We have different types of descriptors, like Constant Buffer View (CBV), Shader Resource View (SRV), Unordered Access Views (UAV), Render Target Views (RTV), Samplers and many more (don’t worry if you don’t understand any of these words, we will get to it). A SRV descriptor for example let’s the GPU know what resource to use (a texture for example), and that it will be used in a shader.
In our example we are dealing with a couple of Descriptors, and in the next tutorials we will see a lot more of them. One is the Vertex Buffer that contains all the vertices we want to render, an Index Buffer containing all the indices (the order of how the vertices should be rendered) and a Constant Buffer that is used send data to our Vertex Shader.
Let’s take a look at our descriptors:
D3D12_VERTEX_BUFFER_VIEW m_vertexBufferView; D3D12_INDEX_BUFFER_VIEW m_indexBufferView; ... ... D3D12_CONSTANT_BUFFER_VIEW_DESC desc;
As you can see, we set the buffer location for our vertex shader to the resource we created earlier, the vertex layout (we had Position and Color data for each vertex) – this defines the size of each vertex, and then we set the total size of the structure by taking the size of the cubeVertices data structure.
Descriptors are primarily placed inside Descriptor Heap, so let’s look at that!
These should (although not always possible) contain all of the descriptors for one or more frames, and can be seen as a collection of descriptors. It can limit what types of Descriptors it can contain to Descriptors of a given type, or a mixed set of descriptor types.
These are a group of Descriptors inside a Descriptor Heap –like an array of Descriptors. The graphics pipeline is accessing resources through a descriptor table in a heap by using an index.
Here we can see that a lot of shaders are getting a view into the heap by using descriptor tables using an index. Each of the descriptors inside the heap (D1 – D10) is describing a texture or a buffer. Each of the tables got an index, and an offset.
Once defined you need to create it using the CreateDescriptorHeap function on the D3D Device. This creates a heap accepting the types: Constant Buffers, Shader Resource Views and Unordered Access Views (UAV), as we will be adding our Constant Buffer to it, and make it visible to shaders:
As you can see, Descriptors of the type CBV, SRV and UAV can share the same Descriptor Heap, while samplers need their own. Vertex Buffers, Index Buffers, Render Targets, Depth Stencil Views and Stream Output Buffers are bound directly on a command list (discussed in the previous tutorial), and thus, not placed inside of a heaps.
Descriptor Tables can be seen as a subset of descriptors in a descriptor heap. It’s an offset and length in the heap, and can store Descriptors of one or more types.
All this concepts look like an overhead, but the reason we use is coming from the way GPUs operate internally. So, by using descriptors, heaps and tables we have more control on the resources and can operate faster in a way GPU sees it. Just the way DirectX 12 is designed.
Shaders can use a Root Signature and Root Parameters to locate a resources they need to access. In other words, the graphics pipeline can access a resources through a root signature by using an index in a descriptor table.
Root Signature is a kind of “view” into the heap (using resource tables as the binocular) containing resources that shaders can use. You define what resources it addresses and what level of access shaders get to them.
I our case, we need to use a Root Signature to give our Vertex Shader access to a set of data stored in a Constant Buffer.
Then we need to set up our Root Signature. We start by giving the Input Assembler Stage access to the constant buffer through our Root Signature, and denies access to the domain, hull, geometry and pixel shader. Then we store the Descriptor Table containing our Constant Buffer in our Root Signature.
Direct3D 12 uses Descriptors to describe a resource for the GPU. A set of descriptors used to render a full frame or more are places inside of a descriptor heap, and a descriptor table is used to easily access a set of descriptors in a descriptor heap.
A Root Signature can be used by the shader to easily access these Descriptor Tables.
These are all used in combination for you as the programmer to handle the resource binding in DirectX 12, giving you full control of each step. You are now responsible for the resource binding!