When we want to perform any sort of rendering technique, it usually involves using a buffer or texture in our shaders. DirectX 12 gives us multiple ways to use resources but what are the differences and which one should you use?
Being aware of your options and what makes you choose a certain resource over another is quite helpful. This is why I’ve decided write this article, to provide a sort of quick reference guide about resources in DirectX 12.
This is by no means “everything there is to know” about resources. Many of these explanations omit details to get the overall concept across.
Instead, you can use this article to get a grasp on these key concepts you should be aware about. Afterwards, things such as implementation and further research should be easier to accomplish. With that in mind, let’s begin!
(ID3D12)Resource
All buffer and textures resources make use of the ID3D12Resource
interface.
Whenever we’re working with resources we will be interacting with this interface in some way.
// One interface to rule them all
ComPtr<ID3D12Resource> vertexBuffer;
ComPtr<ID3D12Resource> materialBuffer;
ComPtr<ID3D12Resource> albedoTexture;
ComPtr<ID3D12Resource> environmentMap;
This interface gives us the ability to read and write to physical memory. It also owns the memory that we allocate. Whenever the interface is released, and the resource is not being used by a command buffer that’s currently being executed on the GPU, the allocated memory will be freed.
Though an ID3D12Resource
interface allows us to read and write memory, it doesn’t contain any information about what the data will be used for. For that, we need a resource view.
Resource Description
Often the first step of working with resources is allocating memory for them. To start this we need to describe the layout of our data through a D3D12_RESOURCE_DESC
. There quite a few parameters involved but most of them are quite intuitive to understand.
Let’s look at an example of setting up a resource description and allocating memory for a Particle buffer:
ComPtr<ID3D12Resource> particleBuffer;
D3D12_RESOURCE_DESC bufferDescription = {};
bufferDescription.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;
bufferDescription.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;
bufferDescription.Flags = D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS;
bufferDescription.Width = particleBufferSize;
bufferDescription.Height = 1;
bufferDescription.DepthOrArraySize = 1;
bufferDescription.MipLevels = 1;
bufferDescription.SampleDesc.Count = 1;
bufferDescripion.SampleDesc.Quality = 0;
CD3DX12_HEAP_PROPERTIES gpuHeap = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT);
device->CreateCommittedResource(&gpuHeap, D3D12_HEAP_FLAG_NONE, &bufferDescription ,
D3D12_RESOURCE_STATE_COMMON, nullptr, IID_PPV_ARGS(&particleBuffer));
An important thing to highlight are resource flags. We are able to pass flags with our resource description which gives extra functionality to our resource. Notables ones are the ALLOW_RENDER_TARGET
and ALLOW_UNOREDERED_ACCESS
flags. The latter of which we will discuss later in this article.
There is also the CD3DX12_RESOURCE_DESC
helper structs make this process less verbose. You can look at this documentation page to look at the different helpers structures for the resource description: CD3DX12_RESOURCE_DESC structure documentation.
D3D12_RESOURCE_DESC particleBufferDescription = CD3DX12_RESOURCE_DESC::Buffer(particleBufferSize);
particleBufferDescription.Flags |= D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS;
Resource View
A resource view (a.k.a descriptor or descriptor view) is an object that allows us to tell the GPU how to interpret our data in a resource into something it could use in a shader or pipeline. Without a resource view the data doesn’t mean anything to our rendering pipeline.
Let’s look at a D3D12_VERTEX_BUFFER_VIEW
. For our data to be interpreted as a buffer of vertices we’ve to share information like the stride (size) of a single vertex, together with the total amount of vertices in the buffer.
With this information the pipeline is able to figure out how many vertices are inside of our buffer and is able to access the appropriate vertices when combined together with an index buffer view.
D3D12_VERTEX_BUFFER_VIEW vertexBufferView;
vertexBufferView.BufferLocation = vertexBuffer->GetGPUVirtualAddress();
vertexBufferView.SizeInBytes = vertexCount * sizeof(Vertex);
vertexBufferView.StrideInBytes = sizeof(Vertex);
An important thing to point out is that for a single resource we can have multiple descriptors. The descriptor doesn’t influence the data itself. Just how it’s interpreted by our hardware. This is why it’s important to set up your resource view properly because otherwise you might run into undefined behaviour.
In a way, a descriptor is just pointer to a resource with some metadata.
Using this analogy, each type of descriptor (CBV, SRV, UAV, VERTEX, RTV etc.) has different metadata to describe our resource in a different way. Each descriptor type comes with their own functionality, benefits and restraints to be aware off. Some of those important things will be mentioned below.
Descriptor Heap
Since every resource will need one or multiple descriptors, DirectX 12 has a dedicated place to store them called Descriptor heaps. Descriptor heaps are basically nothing more than an array of descriptors.
There are different types of descriptor heaps and these directly correlate with the types of resource views it can store. For example, the D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV
, can store Constant Buffer Views, Shader Resource Views and Unordered Access views.
The reason why there are different types of descriptor heaps is due the fact that some descriptors are more complex (and thus larger in size) than others. A notable thing to mention here is that descriptor sizes can also differ per GPU vendor.
Lastly, a descriptor heap is by default not GPU visible. For that functionality we’ve to pass along the D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE
.
ComPtr<ID3D12DescriptorHeap> descriptorHeap;
D3D12_DESCRIPTOR_HEAP_DESC heapDescription = {};
heapDescription.NumDescriptors = 100;
heapDescription.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
heapDescription.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
device->CreateDescriptorHeap(&heapDescription, IID_PPV_ARGS(&descriptorHeap));
unsigned int descriptorSize = device->GetDescriptorHandleIncrementSize(type);
Heaps
Heaps are (pre-)reserved sections of memory to store our resource onto. DirectX 12 has four different heap types to pick from: DEFAULT
, UPLOAD
, READBACK
and CUSTOM
. Whenever we want to allocate any resource, we have to pick on of these heaps.
In general, the majority of your assets are expected on the DEFAULT
heap. This heap has the most bandwidth for the GPU.
An example of allocating memory for a material buffer on the default heap:
ComPtr<ID3D12Resource> materialBuffer;
CD3DX12_HEAP_PROPERTIES gpuHeap = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT);
D3D12_RESOURCE_DESC bufferDescription = CD3DX12_RESOURCE_DESC::Buffer(bufferSize);
device->CreateCommittedResource(&gpuHeap, D3D12_HEAP_FLAG_NONE, &resourceDescription,
D3D12_RESOURCE_STATE_COMMON, nullptr, IID_PPV_ARGS(&materialBuffer)));
Whenever a resource is allocated on the UPLOAD
heap we gain CPU accessibility to the resource. This allows us to directly map data from the CPU to the GPU through the ID3D12Resource::Map
and ID3D12Resource::Unmap
functions.
The READBACK
heap is similar since it also grants CPU accessibility, but is optimized to readback data from the GPU to the CPU.
The CUSTOM
heap allows you to specify the properties for a heap, such as the size of the memory pool. This heap type you won’t usually interact with unless you want to perform specific optimizations or are working in specific conditions (e.g. with multiple adapters).
Subresource(s)
In computer graphics, texture resources are generally not just a single image. Usually we like to create mipmaps of our textures for various reasons.
To make the process of allocating these textures easier DirectX 12 has a concept known as subresources.
Simply put, a subresource can be seen as a single chunk of a “complex” texture resource.
A texture with no mip levels only has a single subresource, which is the original image.
While a texture with three mip levels has three subresources.
Constant Buffer
Constant buffers are a type of buffer that can be very fast when used properly.
They should be used whenever you’ve a set of constant data that you know your shader(s) will consistently use. For example: static transforms, projection matrices, positional data, light information, etc.
Constant buffers work best if all threads in a warp access the same values. They are optimized for these situations. This speed comes with some restraints, some of which you can be quite annoying if you aren’t aware off them:
- Constant buffers can only store up to 64KB.
These buffers are located in something called constant registers, this allows them to be much faster, with the trade off that there is limited space. - Constant buffers need to be 256-byte aligned.
This is a requirement to ensure proper alignment. This also means that no matter what, the minimum size for any constant buffer is 256 bytes.
- Constant buffers have a unique layout on the GPU.
If you’ve worked with constant buffers before, you will already likely know this. The memory layout for constant buffers is different than a C-style struct or a uniform buffer in GLSL.
Luckily, the HLSL Constant Buffer Layout Visualizer made by Maraneshi is an amazing tool to help you understand the constant buffer layout. I sincerely recommend checking it out.
The remark about the layout also the reason why we don’t need much information when setting up a D3D12_CONSTANT_BUFFER_VIEW_DESC
. Since constant buffers already have their own layout we don’t need to provide information about how to interpret our data:
D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDescription = {};
cbvDescription.BufferLocation = buffer->GetGPUVirtualAddress();
cbvDescription.SizeInBytes = bufferSize; // needs to be 256-byte aligned
device->CreateConstantBufferView(&cbvDescription, handle);
An example of how to setup a constant buffer in HLSL:
struct TransformData
{
float4x4 MVP;
float4x4 Model;
};
ConstantBuffer<TransformData> transformData : register(b0);
Shader Resource
Shader resources are quite versatile and will likely be the resource type you interact the most with. A shader resource can be used to represent a texture or buffer. Whether your data gets interpreted as a texture or buffer is determined by the way you set up your shader resource view.
Let’s look at the difference of how a buffer and texture are defined:
Structured Buffer
Unlike a constant buffers, structured buffers can be larger than 64KB and don’t have to be 256-byte aligned. Another useful property is that structured buffers have a C-style layout, meaning that it will be easier for us to match our CPU data layout of our buffer on the GPU.
To define a structured buffer, we have to fill in the D3D12_BUFFER_SRV
struct which resides within the D3D12_SHADER_RESOURCE_VIEW_DESC
. When any value is assigned to the StructureByteStride
field, the shader resource will be interpreted as a structured buffer.
D3D12_SHADER_RESOURCE_VIEW_DESC srvDescription = {};
srvDescription.ViewDimension = D3D12_SRV_DIMENSION_BUFFER;
srvDescription.Format = DXGI_FORMAT_UNKNOWN;
srvDescription.Buffer.NumElements = numberOfParticles;
srvDescription.Buffer.StructureByteStride = sizeof(Particle);
srvDescription.Buffer.Flags = D3D12_BUFFER_SRV_FLAG_NONE;
device->CreateShaderResourceView(particleBuffer.Get(), &srvDescription, handle);
An interesting thing to note here is that we need to set the format of our descriptor to DXGI_UNKNOWN
. This format member is normally used to describe the layout of our texture. But, since we already defined the stride of a single element via the StructureByteStride
, we don’t need define a format.
An example of how you can use and access a structured buffer in HLSL:
struct Material
{
float metallic;
float roughness;
// ...
};
StructuredBuffer<Material> materials: register(t0);
float4 main(PixelIN IN) : SV_TARGET
{
// Similary to an array, you can directly index into a structured buffer
Material mat = materials[meshInfo.materialIndex];
// ...
}
Textures
Textures are also a key part of the shader resources. For this section, the code shown is for a 2D texture with a single subresource, but it’s good to know that with a shader resource view you can describe:
- Textures (1D, 2D, 3D)
- Cubemaps
- Texture Arrays (1D, 2D, 3D)
The DXGI_FORMAT
that we assign to our resource view will describe the pixel layout of our image. For example, if our image uses a single unsigned integer to describe a pixel. We likely want to use DXGI_FORMAT_R8G8B8A8_UNORM
.
Meanwhile, for something such as an HDRi/EXR which uses floats for each channel, we likely want to use something like DXGI_FORMAT_R32G32B32A32_FLOAT
.
D3D12_SHADER_RESOURCE_VIEW_DESC srvDescription = {};
srvDescription.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
srvDescription.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
srvDescription.Format = DXGI_FORMAT_R8G8B8A8_UNORM; // size of 1 pixel == 1 unsigned integer
srvDescription.Texture2D.MipLevels = 1;
device->CreateShaderResourceView(albedoTexture.Get(), &srvDescription, handle);
A keen eye might’ve spotted that the ViewDimension
now is TEXTURE2D
, while the view dimension of our structured buffer was BUFFER
. The view dimension specifies the resource type of the SRV. This member also determines which _SRV
in the union it will choose (e.g. buffer, tex2D, texCube etc.). You can look inside of the D3D12_SHADER_RESOURCE_VIEW_DESC
struct to get a better view of this union.
An example of how you might use a texture in a shader:
Texture2D Diffuse : register(t0);
SamplerState LinearSampler : register(s0);
float4 main(PixelIN IN) : SV_TARGET
{
float3 albedo = Diffuse.Sample(LinearSampler, IN.TexCoord0).rgb;
// ...
}
Unordered Access
Resources with unordered access have Read-Write access in shaders. This functionality is extremely useful for a lot of different techniques.
Unordered access can be used in all sorts of places. In compute shaders you will often encounter some of these resources since we usually have the goal compute some code and write the output into some buffer or texture.
This resource type can even be read/written to simultaneously by multiple threads. But whenever you do this it’s important to use Atomics to avoid generating memory conflicts.
Setting up an unordered access for our resource is quite straightforward. When writing our resource description we have to pass the D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS
flag (check the ‘Resource Description’ section for an example).
Afterwards, we just have create an unordered access view. The description of a UAV is quite similar to that of a SRV, so a lot will likely look familiar.
D3D12_UNORDERED_ACCESS_VIEW_DESC uavDescription = {};
uavDescription.ViewDimension = D3D12_UAV_DIMENSION_BUFFER;
uavDescription.Format = DXGI_FORMAT_UNKNOWN;
// Notice how UAV has the similar union internally as an SRV description
uavDescription.Buffer.NumElements = numberOfElements;
uavDescription.Buffer.StructureByteStride = elementSize;
uavDescription.Buffer.Flags = D3D12_BUFFER_UAV_FLAG_NONE;
device->CreateUnorderedAccessView(buffer.Get(), nullptr, &uavDescription, handle);
Within our shaders remembering the types for resources with unordered access is quite straightforward. We only have to add ‘RW
‘ (which stands for ‘Read-Write’) in front of our regular types. For example, StructuredBuffer
would become RWStructuredBuffer
.
Let’s look at an arbitrary example of updating some particles and writing their position to some 2D texture:
// ...
RWStructuredBuffer<Particle> particles: register(u0);
RWTexture2D<float4> outputTexture: register(u1);
[numthreads(64, 1, 1)]
void main(uint3 dispatchThreadID : SV_DispatchThreadID)
{
// Note you don't have to make a copy, but you could!
Particle p = particles[dispatchThreadID.x];
p.position += settings.gravity * settings.deltaTime;
// Update the particle in the buffer & write it's position on the screen
uint2 screenLocation = CalculateParticleScreenLocation(p.position);
particles[dispatchThreadID.x] = p;
outputTexture[screenLocation] = float4(p.color, 1.0);
}
If you want another reference of the process of writing to a texture with unordered access,
you can look at the example code in my article about Compute Shaders.
Simplified Dictionary
Let’s sum up what we’ve discussed in this article into even simpler descriptions:
- (ID3D12)Resource
The interface which allows us to read and write memory for graphics related resources/objects. - Resource Description
a struct which defines the data layout of our resource so that we can properly allocate space for it. - Resource View
an object which describes to the pipeline how our resource is intended to be used (e.g. as an 2D texture). There are also often referred to as ‘descriptor’ or ‘descriptor view’. - Descriptor heap
a place to store descriptors of a certain type, acts like an array for descriptors. - Heap
a place in physical memory where resources will be allocated, depending on your heap type you get different functionality. - Subresource
a single part/element of a more complex (texture) resource. - Constant Buffer
A type of resource which is very efficient to use in our shaders, but has certain restrictions such as alignment and size. - Shader Resource
A type of resource which can be used for (structured) buffers and textures (in various formats). Likely the most common resource type you will interact with. - Unordered Access
A type of resource which has read-write functionality in shaders. Very commonly used in compute shaders.
Closure
Well, that’s it! These were some of the key concepts about resources in DirectX 12 that I found worth mentioning. I hope it was useful for you. If you happened to encounter and mistakes or have corrections for me, feel free to reach out to me on Bluesky or X.
Good luck with your projects o/
Stefan
Posted December 12th, 2024