Learning D3D12 from D3D11 - Part 2: GPU Resources, Heaps, and Descriptors
Intro
Management of resources (and views of resources) was fairly explicit in D3D11. D3D12 refreshingly walks away from that a bit, with a more general framework for doing that work. The flip-side is that, as with most things in the modern APIs, it takes a bit more work to achieve the same thing you did before. However, with this effort comes heaps (literally!) of control. In this post, I'll detail an interface from which you can build familiar resource types, views of those resources, and how to manage them.GPU Resources
Let's start by creating a base class from which we can derive our different resource types. The following interface contains a set of properties that you will need for any resource you create:class GPUResource { public: GPUResource(ID3D12Resource *resource, D3D12_RESOURCE_STATES usageState); virtual ~GPUResource(); ID3D12Resource *GetResource() { return mResource; } D3D12_GPU_VIRTUAL_ADDRESS GetGpuAddress() { return mGPUAddress; } D3D12_RESOURCE_STATES GetUsageState() { return mUsageState; } void SetUsageState(D3D12_RESOURCE_STATES usageState) { mUsageState = usageState; } bool GetIsReady() { return mIsReady; } void SetIsReady(bool isReady) { mIsReady = isReady; } protected: ID3D12Resource *mResource; D3D12_GPU_VIRTUAL_ADDRESS mGPUAddress; D3D12_RESOURCE_STATES mUsageState; bool mIsReady; };
GPUResource::GPUResource(ID3D12Resource *resource, D3D12_RESOURCE_STATES usageState) { mResource = resource; mUsageState = usageState; mGPUAddress = 0; mIsReady = false; } GPUResource::~GPUResource() { mResource->Release(); mResource = NULL; }
Let's go over the member variables. First, we have the resource itself in the ID3D12Resource. We'll go into how we create these further down. Next is the GPU virtual address (D3D12_GPU_VIRTUAL_ADDRESS) of the resource. This is mostly useful for certain resources on topics that won't be covered directly in this post, but still worth mentioning. After that is probably the most important thing we'll need to track about any resource, its state, of type D3D12_RESOURCE_STATES. Lastly is a bool to track whether or not the resource is ready for usage. Unlike in D3D11, in D3D12 we need to manage resource uploads to the GPU manually, which means creating something like a vertex buffer becomes a bit more complicated. Since a resource may need to wait for an upload before being ready, we want to track that.
Resource State
I'm going to swing back to the resource state (D3D12_RESOURCE_STATES) for a second, since this is a brand new concept when coming from D3D11. If you take a look at the documentation page I linked above, you'll note that it's an enumeration of every type of resource usage you're familiar with. In D3D11, if you wanted to use something like a structured buffer via a shader resource view in one pass, and via an unordered access view in another pass, you simply needed to bind those views and you were good to go. In D3D12, you still have those separate views, but you also need to track your resource's usage state so that you can inform the API layer when you intend to change how you are using it. Without this information, the API has no way of knowing that a previous call that accesses your resource needs to be finished before the next call that accesses it begins. For example, when you write to a structured buffer in one pass and then read from it in the next one. The transition from one state to another is done mostly via transition barriers, which I'll cover more when we get into command lists (where transitions are used). For now, just know that this is why we need to keep track of resource states, so that we can prevent undefined resource access. If you're curious and want to dive really deep into barriers, MJP has you covered.Resource Types
Now that we have a solid base resource class, let's create some familiar types of resources with it. To keep from going too long, I won't go over every type of resource you can create, but I've picked a couple examples that cover a variety of creation and usage, from which you can derive the rest pretty easily. The first is the vertex buffer.class VertexBuffer : public GPUResource { public: VertexBuffer(ID3D12Resource *resource, D3D12_RESOURCE_STATES usageState, uint32 vertexStride, uint32 bufferSize); ~VertexBuffer() override; D3D12_VERTEX_BUFFER_VIEW GetVertexBufferView() { return mVertexBufferView; } private: D3D12_VERTEX_BUFFER_VIEW mVertexBufferView; };
VertexBuffer::VertexBuffer(ID3D12Resource *resource, D3D12_RESOURCE_STATES usageState, uint32 vertexStride, uint32 bufferSize) :GPUResource(resource, usageState) { mGPUAddress = resource->GetGPUVirtualAddress(); mVertexBufferView.StrideInBytes = vertexStride; mVertexBufferView.SizeInBytes = bufferSize; mVertexBufferView.BufferLocation = mGPUAddress; }
As far as the interface goes, a vertex buffer is still about as simple as it gets in D3D12. All you need is a vertex buffer view, which can be set up front. This is what you use to bind the vertex buffer later on via IASetVertexBuffers. Before moving onto other resources, let's look at how we might create a vertex buffer resource.
VertexBuffer *Direct3DContextManager::CreateVertexBuffer(void* vertexData, uint32 vertexStride, uint32 bufferSize) { ID3D12Resource *vertexBufferResource = NULL; D3D12_RESOURCE_DESC vertexBufferDesc; vertexBufferDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER; vertexBufferDesc.Alignment = 0; vertexBufferDesc.Width = bufferSize; vertexBufferDesc.Height = 1; vertexBufferDesc.DepthOrArraySize = 1; vertexBufferDesc.MipLevels = 1; vertexBufferDesc.Format = DXGI_FORMAT_UNKNOWN; vertexBufferDesc.SampleDesc.Count = 1; vertexBufferDesc.SampleDesc.Quality = 0; vertexBufferDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR; vertexBufferDesc.Flags = D3D12_RESOURCE_FLAG_NONE; D3D12_HEAP_PROPERTIES defaultHeapProperties; defaultHeapProperties.Type = D3D12_HEAP_TYPE_DEFAULT; defaultHeapProperties.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN; defaultHeapProperties.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN; defaultHeapProperties.CreationNodeMask = 0; defaultHeapProperties.VisibleNodeMask = 0; Direct3DUtils::ThrowIfHRESULTFailed(mDevice->CreateCommittedResource(&defaultHeapProperties, D3D12_HEAP_FLAG_NONE, &vertexBufferDesc, D3D12_RESOURCE_STATE_COPY_DEST, NULL, IID_PPV_ARGS(&vertexBufferResource))); VertexBuffer *vertexBuffer = new VertexBuffer(vertexBufferResource, D3D12_RESOURCE_STATE_COPY_DEST, vertexStride, bufferSize); VertexBufferBackgroundUpload *vertexBufferUpload = new VertexBufferBackgroundUpload(this, vertexBuffer, vertexData); mUploadContext->AddBackgroundUpload(vertexBufferUpload); return vertexBuffer; }
There's a lot to unpack here, so we can take it section by section. For now you can ignore the Direct3DContextManager bit, it's just the system I use to manage resources and contexts together (contexts will be a separate post). The important part is that we're passing in what you might expect, your vertex data, the vertex stride, and the size of the buffer in bytes. First up is the D3D12_RESOURCE_DESC, which is a single general interface used to define all types of resources you can create. Setting up something like a vertex buffer is pretty simple, you just need to specify that this is a buffer type, and tell it the size of the buffer. Next is the D3D12_HEAP_PROPERTIES, a small structure with big implications, so let's talk about that.
Resource Heaps
D3D12 resources need to come from heaps, either explicitly created, or inherently created by the API. D3D12_HEAP_PROPERTIES describes what kind of heap we want, which depends on the kinds of resources you want to create from it. Unless you're creating a unique type of heap, odds are you'll end up using one of three provided heap types: D3D12_HEAP_TYPE_DEFAULT, D3D12_HEAP_TYPE_UPLOAD, or D3D12_HEAP_TYPE_READBACK. The DEFAULT heap type is used for resources that will receive high GPU bandwidth, but it doesn't give you any CPU access at all, which means you can't write to it directly (no init data, no Map/Unmap). The UPLOAD heap type is for resources that require frequent write access by the CPU, and is also what you want to use when you want to upload data to initialize resources on a DEFAULT heap. UPLOAD heaps don't get the amount of GPU bandwidth that DEFAULT heaps do, so you only want to use it when necessary. READBACK is the last provided heap type, which is for (as you might expect) CPU reads of data written by the GPU. For the purposes of this writeup, I'll only be using DEFAULT and UPLOAD heaps. So, why did I pick the DEFAULT heap for the vertex buffer? It all comes down to access. Vertex buffer data only needs to be set once, and then it'll never be touched by the CPU again, so it doesn't benefit us to create it in the UPLOAD heap. However, this means we'll need to schedule an upload in order to copy that data, which I do at the bottom of the creation function, and will fully cover separately, as it's a giant topic of its own.Resource Creation
Now that we've defined the heap type, we come to the resource creation function CreateCommittedResource. This function creates what's called an implicit heap, which means the heap itself is inaccessible to you as the programmer. The heap is created for you, large enough to encompass your resource based on the info in the D3D12_RESOURCE_DESC, and is where your resource is allocated. The heap itself will be freed when you free that resource. For the purposes of getting started and learning DX12, this is enough to get you going. Ultimately, however, this creation method isn't the best option for all your resources. CreateCommittedResource remains useful for GPU resources that will be around for the duration of the application, or only rarely changed, such as render and depth targets, look-up textures, global buffers, etc. When it comes to resources you expect to be streaming in and out of memory, you'll want to create your own heaps via CreateHeap, and allocate memory for your resources from them with CreatePlacedResource. This introduces complexity into your resource creation model, since you must now manage that heap memory yourself, but it also gives you much more control over where and how that happens. For now these examples will continue using CreateCommittedResource for simplicity of explanation, just be aware that it's something worth further investigation. Lastly, you'll notice that the creation takes a parameter for the initial state of the resource. Here, we set it to D3D12_RESOURCE_STATE_COPY_DEST, to inform the API that we intend to copy memory to this location (done via the upload).Other Resource Types
Next up is another common resource, the ConstantBuffer. Here is what an implementation and creation of this resource might look like:class ConstantBuffer : public GPUResource { public: ConstantBuffer(ID3D12Resource *resource, D3D12_RESOURCE_STATES usageState, uint32 bufferSize, DescriptorHeapHandle constantBufferViewHandle); ~ConstantBuffer() override; void SetConstantBufferData(const void *bufferData, uint32 bufferSize); DescriptorHeapHandle GetConstantBufferViewHandle() { return mConstantBufferViewHandle; } private: void *mMappedBuffer; uint32 mBufferSize; DescriptorHeapHandle mConstantBufferViewHandle; };
ConstantBuffer::ConstantBuffer(ID3D12Resource *resource, D3D12_RESOURCE_STATES usageState, uint32 bufferSize, DescriptorHeapHandle constantBufferViewHandle) :GPUResource(resource, usageState) { mGPUAddress = resource->GetGPUVirtualAddress(); mBufferSize = bufferSize; mConstantBufferViewHandle = constantBufferViewHandle; mMappedBuffer = NULL; mResource->Map(0, NULL, reinterpret_cast<void**>(&mMappedBuffer)); } ConstantBuffer::~ConstantBuffer() { mResource->Unmap(0, NULL); } void ConstantBuffer::SetConstantBufferData(const void *bufferData, uint32 bufferSize) { Application::Assert(bufferSize <= mBufferSize); memcpy(mMappedBuffer, bufferData, bufferSize); }
ConstantBuffer *Direct3DContextManager::CreateConstantBuffer(uint32 bufferSize) { ID3D12Resource *constantBufferResource = NULL; uint32 alignedSize = AlignU32(bufferSize, D3D12_CONSTANT_BUFFER_DATA_PLACEMENT_ALIGNMENT); D3D12_RESOURCE_DESC constantBufferDesc; constantBufferDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER; constantBufferDesc.Alignment = 0; constantBufferDesc.Width = alignedSize; constantBufferDesc.Height = 1; constantBufferDesc.DepthOrArraySize = 1; constantBufferDesc.MipLevels = 1; constantBufferDesc.Format = DXGI_FORMAT_UNKNOWN; constantBufferDesc.SampleDesc.Count = 1; constantBufferDesc.SampleDesc.Quality = 0; constantBufferDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR; constantBufferDesc.Flags = D3D12_RESOURCE_FLAG_NONE; D3D12_HEAP_PROPERTIES uploadHeapProperties; uploadHeapProperties.Type = D3D12_HEAP_TYPE_UPLOAD; uploadHeapProperties.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN; uploadHeapProperties.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN; uploadHeapProperties.CreationNodeMask = 0; uploadHeapProperties.VisibleNodeMask = 0; Direct3DUtils::ThrowIfHRESULTFailed(mDevice->CreateCommittedResource(&uploadHeapProperties, D3D12_HEAP_FLAG_NONE, &constantBufferDesc, D3D12_RESOURCE_STATE_GENERIC_READ, NULL, IID_PPV_ARGS(&constantBufferResource))); D3D12_CONSTANT_BUFFER_VIEW_DESC constantBufferViewDesc = {}; constantBufferViewDesc.BufferLocation = constantBufferResource->GetGPUVirtualAddress(); constantBufferViewDesc.SizeInBytes = alignedSize; DescriptorHeapHandle constantBufferHeapHandle = mHeapManager->GetNewSRVDescriptorHeapHandle(); mDevice->CreateConstantBufferView(&constantBufferViewDesc, constantBufferHeapHandle.GetCPUHandle()); ConstantBuffer *constantBuffer = new ConstantBuffer(constantBufferResource, D3D12_RESOURCE_STATE_GENERIC_READ, alignedSize, constantBufferHeapHandle); constantBuffer->SetIsReady(true); return constantBuffer; }
There are a few new concepts here. The very first thing you'll see is a DescriptorHeapHandle being passed to the ConstantBuffer constructor. This is a wrapper around D3D12's descriptor handles, which we'll talk about further below, but the variable name "constantBufferViewHandle" probably gives you a good idea of what it's for. Next, in the ConstantBuffer constructor/destructor, you'll see that the resource itself is mapped for its entire lifetime. This is new to us coming from D3D11, when your writes would only take effect once you unmapped your memory. Resources created on upload heaps (see the creation function) can be persistently mapped, allowing you to write to memory as you see fit. Those changes will be visible to the GPU, which means that care must be taken to only modify the data in a buffer like this when you know the GPU isn't using it. A trivial way to do this is to double-buffer your constant buffers (in the case like mine where I'm double buffering my frames), that way you're not modifying the constant buffer being consumed by the frame that's being processed, but rather the constant buffer that will be used when you submit your current frame's work.
Next, in the creation function, note the alignment function at the top. Constant buffers must be aligned to D3D12_CONSTANT_BUFFER_DATA_PLACEMENT_ALIGNMENT (256-byte aligned) in D3D12. Apart from that, the rest is similar to the vertex buffer. Note again that we're creating this on the upload heap, which is how we're able to map the buffer and modify it on the CPU side. Constant buffers are a good use case for upload-heap resources, since they're likely to be modified frame to frame and need frequent writes from the CPU. Unlike the vertex buffer, we set the initial state to D3D12_RESOURCE_STATE_GENERIC_READ, which is the required initial state for any resource created in an upload heap.
Finally, the last major difference is that we need to create a constant buffer view for this new buffer. We still use views similarly to D3D11, but the way we create them and bind them are different now. Let's talk about that.
Descriptors
In the land of D3D12, we now reference the majority of our shader resources via something called descriptor handles, which come from descriptor heaps. Descriptor handles are, simply, handles to your resources that are also ultimately used to describe their usage. Since we'll be needing tons of these, it makes sense that these come from heaps. Here's how we might wrap one of these generic handles:class DescriptorHeapHandle { public: DescriptorHeapHandle() { mCPUHandle.ptr = NULL; mGPUHandle.ptr = NULL; mHeapIndex = 0; } D3D12_CPU_DESCRIPTOR_HANDLE GetCPUHandle() { return mCPUHandle; } D3D12_GPU_DESCRIPTOR_HANDLE GetGPUHandle() { return mGPUHandle; } uint32 GetHeapIndex() { return mHeapIndex; } void SetCPUHandle(D3D12_CPU_DESCRIPTOR_HANDLE cpuHandle) { mCPUHandle = cpuHandle; } void SetGPUHandle(D3D12_GPU_DESCRIPTOR_HANDLE gpuHandle) { mGPUHandle = gpuHandle; } void SetHeapIndex(uint32 heapIndex) { mHeapIndex = heapIndex; } bool IsValid() { return mCPUHandle.ptr != NULL; } bool IsReferencedByShader() { return mGPUHandle.ptr != NULL; } private: D3D12_CPU_DESCRIPTOR_HANDLE mCPUHandle; D3D12_GPU_DESCRIPTOR_HANDLE mGPUHandle; uint32 mHeapIndex; };
Overall not a lot of data, but there are a few concepts to go over here. First, note the CPU and GPU handles. Descriptors always have visibility on the CPU, so when you're assigning handles from your heap, they will always have CPU handles. If the heap is visible to shaders, meaning they can be accessed by shaders, then they will also have associated GPU handles. You're probably wondering why you would ever need handles (resource views) that aren't visible to shaders, but there's good reason for that. You need to bind descriptor heaps when you issue commands that wish to reference resources in shaders, as you might expect. The difference, however, is that these handles must be contiguous in the same heap in order to be referenced in ranges by a shader. For example, if you have a range of textures in a shader from t0-t2, the shader resource view handles must be contiguous in a heap so that they can be bound to a descriptor table, which is generally how things are bound in D3D12, rather than binding individual views. In order to make this process easier, you can create your initial views in descriptor handles at resource creation time from non-shader-visible descriptor heaps using whichever handles are available, and later copy those handles into shader-visible heaps that will be bound for rendering, so that you can assemble them to be contiguous. To see how we might do that, let's first make a wrapper we can use for any kind of descriptor heap, so we can see how we use them.
class DescriptorHeap { public: DescriptorHeap(ID3D12Device *device, D3D12_DESCRIPTOR_HEAP_TYPE heapType, uint32 numDescriptors, bool isReferencedByShader); virtual ~DescriptorHeap(); ID3D12DescriptorHeap *GetHeap() { return mDescriptorHeap; } D3D12_DESCRIPTOR_HEAP_TYPE GetHeapType() { return mHeapType; } D3D12_CPU_DESCRIPTOR_HANDLE GetHeapCPUStart() { return mDescriptorHeapCPUStart; } D3D12_GPU_DESCRIPTOR_HANDLE GetHeapGPUStart() { return mDescriptorHeapGPUStart; } uint32 GetMaxDescriptors() { return mMaxDescriptors; } uint32 GetDescriptorSize() { return mDescriptorSize; } protected: ID3D12DescriptorHeap *mDescriptorHeap; D3D12_DESCRIPTOR_HEAP_TYPE mHeapType; D3D12_CPU_DESCRIPTOR_HANDLE mDescriptorHeapCPUStart; D3D12_GPU_DESCRIPTOR_HANDLE mDescriptorHeapGPUStart; uint32 mMaxDescriptors; uint32 mDescriptorSize; bool mIsReferencedByShader; };
DescriptorHeap::DescriptorHeap(ID3D12Device *device, D3D12_DESCRIPTOR_HEAP_TYPE heapType, uint32 numDescriptors, bool isReferencedByShader) { mHeapType = heapType; mMaxDescriptors = numDescriptors; mIsReferencedByShader = isReferencedByShader; D3D12_DESCRIPTOR_HEAP_DESC heapDesc; heapDesc.NumDescriptors = mMaxDescriptors; heapDesc.Type = mHeapType; heapDesc.Flags = mIsReferencedByShader ? D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE : D3D12_DESCRIPTOR_HEAP_FLAG_NONE; heapDesc.NodeMask = 0; Direct3DUtils::ThrowIfHRESULTFailed(device->CreateDescriptorHeap(&heapDesc, IID_PPV_ARGS(&mDescriptorHeap))); mDescriptorHeapCPUStart = mDescriptorHeap->GetCPUDescriptorHandleForHeapStart(); if (mIsReferencedByShader) { mDescriptorHeapGPUStart = mDescriptorHeap->GetGPUDescriptorHandleForHeapStart(); } mDescriptorSize = device->GetDescriptorHandleIncrementSize(mHeapType); } DescriptorHeap::~DescriptorHeap() { mDescriptorHeap->Release(); mDescriptorHeap = NULL; }
Descriptor heaps are quick to get up and running. The information needed to create the heap are the heap type, the number of descriptors in the heap, and whether or not it should be shader-visible. There are four different heap types (see the link). The importance of the heap type is that it determines the increment size from one descriptor handle to the next in a descriptor heap, which you'll see I've stored off in mDescriptorSize. Different heap types result in different sizes. We also store the starting CPU descriptor handle and (if shader-visible) the starting GPU descriptor handle. All of this information is needed in order to give out handles from the heap, by taking the heap start and offsetting by the descriptor size, which we'll see below in our first descriptor heap implementation. The StagingDescriptorHeap is a non-shader-visible descriptor heap from which we can request handles at resource creation time as needed, and return the handles when we're done with them. Descriptor handles can be reused for different resource views when the previous view is no longer being used, so we want to take advantage of that.
class StagingDescriptorHeap : public DescriptorHeap { public: StagingDescriptorHeap(ID3D12Device *device, D3D12_DESCRIPTOR_HEAP_TYPE heapType, uint32 numDescriptors); ~StagingDescriptorHeap() final; DescriptorHeapHandle GetNewHeapHandle(); void FreeHeapHandle(DescriptorHeapHandle handle); private: DynamicArray<uint32> mFreeDescriptors; uint32 mCurrentDescriptorIndex; uint32 mActiveHandleCount; };
StagingDescriptorHeap::StagingDescriptorHeap(ID3D12Device *device, D3D12_DESCRIPTOR_HEAP_TYPE heapType, uint32 numDescriptors) :DescriptorHeap(device, heapType, numDescriptors, false) { mCurrentDescriptorIndex = 0; mActiveHandleCount = 0; } StagingDescriptorHeap::~StagingDescriptorHeap() { if (mActiveHandleCount != 0) { Direct3DUtils::ThrowRuntimeError("There were active handles when the descriptor heap was destroyed. Look for leaks."); } mFreeDescriptors.Clear(); } DescriptorHeapHandle StagingDescriptorHeap::GetNewHeapHandle() { uint32 newHandleID = 0; if (mCurrentDescriptorIndex < mMaxDescriptors) { newHandleID = mCurrentDescriptorIndex; mCurrentDescriptorIndex++; } else if (mFreeDescriptors.CurrentSize() > 0) { newHandleID = mFreeDescriptors.RemoveLast(); } else { Direct3DUtils::ThrowRuntimeError("Ran out of dynamic descriptor heap handles, need to increase heap size."); } DescriptorHeapHandle newHandle; D3D12_CPU_DESCRIPTOR_HANDLE cpuHandle = mDescriptorHeapCPUStart; cpuHandle.ptr += newHandleID * mDescriptorSize; newHandle.SetCPUHandle(cpuHandle); newHandle.SetHeapIndex(newHandleID); mActiveHandleCount++; return newHandle; } void StagingDescriptorHeap::FreeHeapHandle(DescriptorHeapHandle handle) { mFreeDescriptors.Add(handle.GetHeapIndex()); if (mActiveHandleCount == 0) { Direct3DUtils::ThrowRuntimeError("Freeing heap handles when there should be none left"); } mActiveHandleCount--; }
When we need a new handle, we simply take the index of the next descriptor available, or one that has been returned in the free descriptor list. We then use that index to make a new handle by offsetting from the heap starting point. We also store off that heap index to make it easy to return descriptor handles for reuse in the FreeHeapHandle function. Earlier you saw me make a call to GetNewSRVDescriptorHeapHandle (CBV/SRV/UAV heap) to get a new handle for the constant buffer creation. This was just accessing the CBV/SRV/UAV StagingDescriptorHeap and calling GetNewHeapHandle. To start off, you'll want to create a StagingDescriptorHeap for each heap type, as needed. Check out this link for more information on non-shader-visible descriptor heaps, and note that the only shader visible heaps you will have are for CBVs, SRVs, UAVs, and samplers, as all other resources types have their descriptor contents recorded directly into command lists via specific API calls, rather than descriptor tables. You'll also want to visit the hardware limit page to see the limits of individual heap types. For example, the maximum number of samplers in a shader visible heap is 2048. Speaking of shader visible heaps, let's create a wrapper for how we might use them:
class RenderPassDescriptorHeap : public DescriptorHeap { public: RenderPassDescriptorHeap(ID3D12Device *device, D3D12_DESCRIPTOR_HEAP_TYPE heapType, uint32 numDescriptors); ~RenderPassDescriptorHeap() final; void Reset(); DescriptorHeapHandle GetHeapHandleBlock(uint32 count); private: uint32 mCurrentDescriptorIndex; };
RenderPassDescriptorHeap::RenderPassDescriptorHeap(ID3D12Device *device, D3D12_DESCRIPTOR_HEAP_TYPE heapType, uint32 numDescriptors) :DescriptorHeap(device, heapType, numDescriptors, true) { mCurrentDescriptorIndex = 0; } DescriptorHeapHandle RenderPassDescriptorHeap::GetHeapHandleBlock(uint32 count) { uint32 newHandleID = 0; uint32 blockEnd = mCurrentDescriptorIndex + count; if (blockEnd < mMaxDescriptors) { newHandleID = mCurrentDescriptorIndex; mCurrentDescriptorIndex = blockEnd; } else { Direct3DUtils::ThrowRuntimeError("Ran out of render pass descriptor heap handles, need to increase heap size."); } DescriptorHeapHandle newHandle; D3D12_CPU_DESCRIPTOR_HANDLE cpuHandle = mDescriptorHeapCPUStart; cpuHandle.ptr += newHandleID * mDescriptorSize; newHandle.SetCPUHandle(cpuHandle); D3D12_GPU_DESCRIPTOR_HANDLE gpuHandle = mDescriptorHeapGPUStart; gpuHandle.ptr += newHandleID * mDescriptorSize; newHandle.SetGPUHandle(gpuHandle); newHandle.SetHeapIndex(newHandleID); return newHandle; } void RenderPassDescriptorHeap::Reset() { mCurrentDescriptorIndex = 0; }
I call this a RenderPassDescriptorHeap to indicate its intended usage. You'll notice that we're no longer requesting just individual handles, but potentially blocks of multiple handles. The handle setup logic is basically the same, with the addition of the GPU handle. We also aren't bothering to return handles, because we'll be using them contiguously as I described earlier, until we reach the end of a frame. The next time a RenderPassDescriptorHeap is used, it should call Reset() first to start back at the beginning of the heap. If you have N bufferable frames of work, you will need to buffer each RenderPassDescriptorHeap N times, because if you modify the contents of a shader visible descriptor heap while the GPU is using it, you will cause undefined behavior. So for any given RenderPassDescriptorHeap, it will be used, then N frames later it will call Reset and can be used again. So then, what does this usage look like, and how do we get our non-shader-visible handles (made during resource creation from StagingDescriptorHeaps) into our shader-visible RenderPassDescriptorHeaps?
Let's say we have two constant buffers (implemented as above) needed for a shader, in a single range from b0-b1.
ConstantBuffer *mBuffer1; ConstantBuffer *mBuffer2;
And for brevity's sake, let's say we've already created these resources as shown above, complete with a CBV descriptor handle from the StagingDescriptorHeap and have been mapped with data. When the time comes to need the handles in a shader visible heap that has been bound for rendering via SetDescriptorHeaps, we would do the following.
DescriptorHeapHandle cbvBlockStart = cbvHeap->GetHeapHandleBlock(2); D3D12_CPU_DESCRIPTOR_HANDLE currentCBVHandle = cbvBlockStart.GetCPUHandle(); uint32 cbvDescriptorSize = cbvHeap->GetDescriptorSize(); device->CopyDescriptorsSimple(1, currentCBVHandle, mBuffer1->GetConstantBufferViewHandle().GetCPUHandle(), D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV); currentCBVHandle.ptr += cbvDescriptorSize; device->CopyDescriptorsSimple(1, currentCBVHandle, mBuffer2->GetConstantBufferViewHandle().GetCPUHandle(), D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
CopyDescriptorsSimple takes care of copying the non-shader-visible handles to the shader-visible ones. Now, the shader-visible heap has handles to those constant buffers that it can use, in contiguous locations. To bind them, you would set the descriptor table in the command list with the GPU handle of the block start. For example:
commandList->SetGraphicsRootDescriptorTable([index], cbvBlockStart->GetGPUHandle());
And that's everything! I'll show more about the binding process later when I go over command lists, but this post has shown what you need in order to get from resource creation to resource binding.
Final Notes and Thoughts
I want to mention that this is not the only way to set yourself up for binding. If you have a really good understanding of how your resources will be bound, you can potentially forego the non-shader-visible heaps altogether. You can even go for bindless style rendering (see a link on that in the Resources section below). I've done it the way I did it above because it offers the most flexibility with your resources, and because it feels most familiar to DX11 practices. Once you have a better understanding, it's worth exploring this deeper and see if other methodologies work better for you.Something else you may have noticed in the Microsoft documents above (or other documents) is that it recommends against switching shader-visible heaps via SetDescriptorHeaps mid-frame, which would also be a suggestion against having multiples of the RenderPassDescriptorHeap for a given heap type. The reason it warns against doing this is because swapping descriptor heaps on certain cards can cause a GPU wait for previous commands to finish before swapping the heap, which would damage your performance. However, descriptor heaps on some cards are implemented with a singular massive hardware descriptor heap that you are suballocating out of when you create new descriptor heaps. This means you'll only incur the performance cost if you go over the hardware limit for descriptor heap sizes for a given type, which on all recent hardware (for the CBV/SRV/UAV heap) is over 1 million descriptors. Not all IHVs do it this way though, for example on Intel cards, you will take a noticeable hit to performance if you use multiple descriptor heaps per frame.
Resources
The following are a list of resources from which I pull the vast majority of my information and design from. It's thanks to these resources that I feel comfortable with DX12 enough to be able to share, so thank you to everyone who contributes to these!The Microsoft DirectX Graphics Sample Github, especially the MiniEngine core. I borrow heavily from this.
The DirectXTech Forum "Getting Started" Section.
MJP's awesome bindless deferred rendering github. His resource upload style is great.
Microsoft's Direct3D 12 Programming Guide.
The D3D12 Reference Docs.
Nvidia's DX12 Do's and Don'ts, especially useful at answering "how should I do this?" questions.
Intel's DX12 Migration Tutorials.