# Direct3D 12: Long Way to Access Data

Sun
30
Jan 2022

One reason the new graphics APIs – Direct3D 12 and Vulkan – are so complicated is that there are so many levels of indirection in accessing data. Having some data prepared in a normal C++ CPU variable, we have to go a long way before we can access it in a shader. Some time ago I’ve written an article: “Vulkan: Long way to access data”. Because I have a privilege of working with both APIs in my everyday job, I thought I could also write a D3D12 equivalent, so here it is. If you are a graphics or game programmer who already know the basics of D3D12, but may feel some confusion about all the concepts defined there, this article is for you.

Here is a diagram, followed by an explanation of all its elements:

Let's start from the final destination of our data – the shader. We are in the realm of the shader code written in HLSL language, denoted by yellow background. Consider an example of a vertex shader, where we want to multiply vertex position by a combined world-view-projection matrix, to transform it from local model space (this is how vertices of a model are stored in memory) to DirectX clip space (this is the space required for vertex positions on the vertex shader output). Because our matrix is 4x4, we extend the 3-component vertex position to homogenous coordinates by adding 4th component equal to 1 – a thing that is out of scope of this article.

`output.pos = mul(perObjectConstants.worldViewProj, float4(pos, 1));`

Here we have a first entity, a first name for our data: `perObjectConstants.worldViewProj`. This is a symbol valid only inside the code of our vertex shader, defined in its global scope as a constant buffer:

`struct PerObjectConstants{    float4x4 worldViewProj;};ConstantBuffer<PerObjectConstants> perObjectConstants : register(b3, space0);`

Here is a place where we cross the realms from yellow to green background. On the shader side, this specific constant buffer is referred as `perObjectConstants`. For the outside world, we give it another identifier: “register b3 in space 0”. Registers (or slots) where we bind various resources to be accessible to shaders is a concept known in many graphics APIs for a long time.

In D3D12, the register pool is actually 2-dimensional, with the second dimension called “space”. This is because we could define a resource as an unbounded array that has no defined limit on the number of elements, so all higher slots are reserved and so we have to jump to a higher space to have more slots available – but this is out of scope of this article. We normally just use space 0. In this case, the `space0` part in HLSL code could be omitted.

Moving to the realm of C++ code that uses D3D12, we refer to our constant buffer in a root signature. This is an object that describes all the resources that will be bound to our vertex, pixel, and other shaders. Root signature can be defined programmatically by filling structure `D3D12_ROOT_SIGNATURE_DESC`, calling function `D3D12SerializeRootSignature` to get a raw data buffer with its serialized content, then pass this to `device->CreateRootSignature` to create a `ID3D12RootSignature` object, which later becomes part of a pipeline state object (`ID3D12PipelineState`). It also has to be set in a recorded command list by calling `ID3D12GraphicsCommandList::SetGraphicsRootSignature`. Alternatively, a root signature can be defined inside HLSL code, using a not-so-pretty syntax in form of a string.

But this is all a side note here. What is important to us is that after we define a root parameter describing our constant buffer in register `b3`, we move to another representation of it. From now on, we will refer to the constant buffer by the index at which we define our root parameter in the root signature, for example: 2:

`D3D12_ROOT_PARAMETER params[10];// ...params[2].ParameterType = D3D12_ROOT_PARAMETER_TYPE_32BIT_CONSTANTS;params[2].ShaderVisibility = D3D12_SHADER_VISIBILITY_VERTEX;params[2].Constants.ShaderRegister = 3;params[2].Constants.RegisterSpace = 0;params[2].Constants.Num32BitValues = 16;// ...`

Here, our diagram branches in to 3 options, marked as A), B), C). These are 3 different ways of passing a constant buffer, which differ in the number of levels of indirection. We choose it by setting appropriate values in the `D3D12_ROOT_PARAMETER::ParameterType` member. Let’s start with the easiest one A), which is called “32BIT_CONSTANTS”, also known as “root constants”. As you can see in the code above, we only specify register, space, and the number of 32-bit values, which is 16 in case of a 4x4 matrix of `float` numbers. `ShaderVisibility` is common to all types of root parameters and can be a specific shader stage (like vertex shader in this example) or `D3D12_SHADER_VISIBILITY_ALL`. Strangely, this is not a bit flag where we could specify any combination of shader stages. But this is unimportant here.

The reason this approach A) is the simplest is because we can pass the data straight from a pointer to a C++ variable while recording a command list, like this:

`glm::mat4 worldViewProj = ...commandList->SetGraphicsRoot32BitConstants(    2, // RootParameterIndex    16, // Num32BitValuesToSet    &worldViewProj, // pSrcData    0); // DestOffsetIn32BitValues`

Note the “CPU_data” in the diagram belong to the realm of “Preparation (CPU)” (blue background), which means the pointer doesn’t need to remain valid for the time when the command list is executed on the GPU, only for the call to `SetGraphicsRoot32BitConstants`. This function makes a copy of the pointed data and stores them inside the command list, in between draw calls and other commands that we are recording.

Because of this, it is recommended not to over-use this feature. It is better to avoid it or use it only for passing some small amount of data that really changes every draw call. Some index of an instance that a shader will use to refer to some larger record fetched from a buffer may be a good application of the “32BIT_CONSTANTS” root parameter type. Passing a whole 4x4 matrix, as we do in this example, is not so much.

So let’s see the option B), called “CBV”, also known as “root descriptor”. All we need to do when defining the root signature is to specify that register 3 space 0 will be of type `CBV`:

`params[2].ParameterType = D3D12_ROOT_PARAMETER_TYPE_CBV;params[2].ShaderVisibility = D3D12_SHADER_VISIBILITY_VERTEX;params[2].Descriptor.ShaderRegister = 3;params[2].Descriptor.RegisterSpace = 0;`

Here we have an additional level of indirection involved – a real constant buffer. Note that in approach A) we didn’t have any such thing, despite we defined `ConstantBuffer` in the shader code. This is because it was kind of shortcut, an “imaginary constant buffer” created by D3D12 from directly passed root constants. Now we will have a real constant buffer object. While recording our command buffer, we have to set our root argument 2 to the address of that constant buffer, using function `ID3D12GraphicsCommandList::SetGraphicsRootConstantBufferView`.

Note that words “descriptor” and “view” can be used somewhat interchangeably when discussing D3D12. Unlike in Vulkan, “views” are not separate objects here. Functions called `Create*View` just setup descriptors in D3D12.

`ID3D12Resource* constBuf = ...D3D12_GPU_VIRTUAL_ADDRESS constBufGPUAddr = constBuf->GetGPUVirtualAddress();commandList->SetGraphicsRootConstantBufferView(    2, // RootParameterIndex    constGPUBufAddr); // BufferLocation`

`D3D12_GPU_VIRTUAL_ADDRESS` is just a `typedef` to `UINT64`. D3D12, more often than Vulkan, allows to manipulate raw memory addresses. Thanks to this, there is no need for a separate parameter for an offset from the beginning of the buffer to the beginning of the data we want to bind. We can just increment the address by the right amount of bytes. This is a big upgrade compared to Direct3D 11 with functions like `ID3D11DeviceContext::VSSetConstantBuffers`, where there was no offset or address, so every data record had to have its own buffer to start at the beginning. Only a later update to the API in DirectX 11.1 added function `ID3D11DeviceContext1::VSSetConstantBuffers1` that accepted an offset, allowing to bundle many records of data in a single buffer object.

Back to the DirectX 12, we now need to discuss how a constant buffer is created. Actually, there is no such concept as a “constant buffer” per se. In D3D12 there are just “buffers” and “textures”, together called “resources”. To create a resource, we have to allocate some memory for it. There are two ways to do it, again differing in the number of levels of indirection.

First and the easiest option to create a resource is to call `ID3D12Device::CreateCommittedResource`. This function creates a buffer or texture and allocates memory for it in one call. The resource will then have its own dedicated memory block. We do it like this:

`D3D12_HEAP_PROPERTIES heapProps = {};heapProps.Type = D3D12_HEAP_TYPE_UPLOAD;D3D12_RESOURCE_DESC resDesc = {};resDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;resDesc.Alignment = 0;resDesc.Width = sizeof(glm::mat4);resDesc.Height = 1;resDesc.DepthOrArraySize = 1;resDesc.MipLevels = 1;resDesc.Format = DXGI_FORMAT_UNKNOWN;resDesc.SampleDesc.Count = 1;resDesc.SampleDesc.Quality = 0;resDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;resDesc.Flags = D3D12_RESOURCE_FLAG_NONE;ID3D12Resource* constBuf;HRESULT res = device->CreateCommittedResource(    &heapProps,    D3D12_HEAP_FLAG_NONE,    &resDesc,    D3D12_RESOURCE_STATE_COPY_DEST, // InitialResourceState    nullptr, // pOptimizedClearValue    IID_PPV_ARGS(&constBuf));`

Structure `D3D12_HEAP_PROPERTIES` describes the memory where we want to allocate our buffer, e.g. `D3D12_HEAP_TYPE_DEFAULT` in case of GPU memory and `D3D12_HEAP_TYPE_UPLOAD` in case of system memory. Structure `D3D12_RESOURCE_DESC` describes all the parameters of our resource. In case of a buffer, most of them take some default values, like in this example. Only `Width` is the proper length of the buffer, in bytes. All other members take some meaningful values when we create textures, as they have a more complex structure with more parameters needed to describe them. But in this article we focus on buffers.

Creating all the resources this way is fine in D3D12, as we don’t have a small upper limit on the number of allocations we can make – unlike in Vulkan, where the limit is only 4096 on most GPUs. Nonetheless, memory allocation can be a slow operation, so it might be more efficient to pre-allocate larger memory blocks and dedicate parts of them to our small textures and buffers. <CommercialBreak> There is a free library that can help with this: D3D12 Memory Allocator from AMD :) </CommercialBreak>

If we want to do it by ourselves, we take the second path, where another object appears – a `ID3D12Heap`. It represents a block of memory allocated by D3D12. Having such heap and an offset at which we want to place our new buffer inside of it, we can create the buffer like this, using function `ID3D12Device::CreatePlacedResource`, which could be much faster:

`ID3D12Heap* heap = ...UINT64 offsetInHeap = ...ID3D12Resource* constBuf;HRESULT res = device->CreatePlacedResource(    heap,    offsetInHeap,    &resDesc,    D3D12_RESOURCE_STATE_COPY_DEST, // InitialResourceState    nullptr, // pOptimizedClearValue    IID_PPV_ARGS(&constBuf));`

Now we have to learn how to allocate the heap. This is quite easy. The only caveat is that we should check `D3D12_FEATURE_DATA_D3D12_OPTIONS::ResourceHeapTier` during initialization of our app. When it is `D3D12_RESOURCE_HEAP_TIER_2`, we can freely mix all kinds of resources in one heap. However, if we find its value to be `D3D12_RESOURCE_HEAP_TIER_1` (e.g. on NVIDIA GeForce GTX 900 series and earlier), we have to keep 3 types of resources (buffers, render target or depth stencil textures, other textures) in separate heaps and create them with flags like `D3D12_HEAP_FLAG_ALLOW_ONLY_BUFFERS` in this example. The D3D12MA library would do it automatically.

`D3D12_HEAP_DESC heapDesc = {};heapDesc.SizeInBytes = 64ull * 1024 * 1024; // 64 MBheapDesc.Properties.Type = D3D12_HEAP_TYPE_DEFAULT;heapDesc.Alignment = D3D12_DEFAULT_RESOURCE_PLACEMENT_ALIGNMENT;heapDesc.Flags = D3D12_HEAP_FLAG_ALLOW_ONLY_BUFFERS;ID3D12Heap* heap;HRESULT res = device->CreateHeap(&heapDesc, IID_PPV_ARGS(&heap));`

To have the whole approach B) fully described, the only remaining part is the way to transfer our data from raw CPU `void*` pointer to the constant buffer. Again, there are two ways to do it. If the buffer was created in the CPU memory, like `D3D12_HEAP_TYPE_UPLOAD`, we can call `ID3D12Resource::Map` to “map” its memory, get a raw CPU pointer to its data and just do `memcpy`. GPU executing our shader can then read straight from this buffer, but the access will cause transfers over PCI Express bus, which may be slow.

The other option is to have another “staging” buffer created in CPU memory (`D3D12_HEAP_TYPE_UPLOAD`), our main buffer created in GPU memory (`D3D12_HEAP_TYPE_DEFAULT`) and explicitly issue a copy operation between them, to be executed at the optimal moment. It can be a call to `ID3D12GraphicsCommandList::CopyResource` or some other copy function, e.g. `CopyBufferRegion`.

Note that in the diagram, “Map()” lies on the side of “Preparation (CPU)”, which means it happens at the exact moment we call this code in C++. On the other hand, “CopyResource()” is a command recorded to the command list and executed later by the GPU, so it lies on the green side. We can record such copy operation to a command list executed on the graphics queue, to happen at the exact moment between our draw calls, or do it on copy queue or async compute queue, to happen asynchronously. You can watch my talk “Efficient Use of GPU Memory in Modern Games” to learn which way is more efficient in what cases.

Another important thing to note is that in the diagram, “constBuf” lies on the edge between the realm of command list execution and the pure CPU code. It means that the buffer itself (the one that we pass as the root argument) and the data inside of it (at least the part that we read in the shader) must remain alive and unchanged from the moment we submit the command buffer for execution until we are sure it finished executing on the GPU. Otherwise, bad things may happen, resulting in non-deterministic, hard to find bugs. We can ensure this e.g. by allocating and freeing data in a ring-buffer fashion.

It is time to move on to the last approach C), called “DESCRIPTOR_TABLE”. It will use everything we learned so far plus additional level of indirection in form of a descriptor heap. Although the most complicated, this is also the recommended way for typical use cases, as it puts the least pressure on the precious space in the root signature. One root parameter can describe the an entire set of shader registers and they can be even indexed dynamically in the shader (watch out for NonUniformResourceIndex when doing this!). Still, we will limit ourselves to just a single register `b3` in this example, for simplicity.

`D3D12_DESCRIPTOR_RANGE descRange = {};descRange.RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_CBV;descRange.NumDescriptors = 1;descRange.BaseShaderRegister = 3;descRange.RegisterSpace = 0;descRange.OffsetInDescriptorsFromTableStart = 0;params[2].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE;params[2].ShaderVisibility = D3D12_SHADER_VISIBILITY_VERTEX;params[2].DescriptorTable.NumDescriptorRanges = 1;params[2].DescriptorTable.pDescriptorRanges = &descRange;`

This time the `ParameterType` is `D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE`. We have another (surprise!) level of indirection here which allows us to specify an entire array of “descriptor ranges”, but we specify just one. Each descriptor range specifies shader register, space, number of descriptors, and an additional offset, which I will mention later.

Now we must create `descriptorHeap`. An object of type `ID3D12DescriptorHeap` is a special piece of D3D12 memory allocated to store descriptors. In other words, what we’ve set directly as a “CBV” root argument in the approach B), we will now store in a special place that the root signature will access. While creating the descriptor heap, we need to specify the type of descriptors we need to store (either CBV + SRV + UAV, SAMPLER, RTV, or DSV) and its maximum capacity. Having such an object, we can just refer to its content by indexing the descriptors.

`D3D12_DESCRIPTOR_HEAP_DESC descriptorHeapDesc = {};descriptorHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;descriptorHeapDesc.NumDescriptors = 1000;descriptorHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;descriptorHeapDesc.NodeMask = 0;ID3D12DescriptorHeap* descriptorHeap;HRESULT res = device->CreateDescriptorHeap(    &descriptorHeapDesc, IID_PPV_ARGS(&descriptorHeap)));`

To set our root argument 2 to use a specific descriptor from our newly created descriptor heap, we have to call two functions. First, `ID3D12GraphicsCommandList::SetDescriptorHeaps`, just sets the whole descriptor heap as the current one. There can be only one descriptor heap of type `CBV_SRV_UAV` and one of type `SAMPLER` set at a time, so you must keep all the descriptors you may need for a draw call in one big heap.

`commandList->SetDescriptorHeaps(1, &descriptorHeap);`

The second call is to function `ID3D12GraphicsCommandList::SetGraphicsRootDescriptorTable`. It sets our specific root argument 2 to point to a specific descriptor in the heap. One caveat is that the function expects a `D3D12_GPU_DESCRIPTOR_HANDLE`, which is a structure containing (again!) a raw memory address. We can fetch an address of the beginning of the heap by calling `ID3D12DescriptorHeap::GetGPUDescriptorHandleForHeapStart`. Then we need to increment it by the index of our descriptor in the heap, multiplied by a size of a descriptor, as the address is expressed in bytes.

Descriptors of a specific kind have a constant size that can be fetched using function `ID3D12Device::GetDescriptorHandleIncrementSize`. We can do it just once, during program startup. The value is implementation-defined, but we can expect it to be fairly small. For example, on the PC that I now have handy, with GeForce RTX 2080 Ti card installed, the `CBV_SRV_UAV` descriptor size is 32 bytes, so there should be no problem to use a heap capable of storing many thousands of such descriptors.

`D3D12_GPU_DESCRIPTOR_HANDLE GPUDescriptorHandle =    descriptorHeap->GetGPUDescriptorHandleForHeapStart();GPUDescriptorHandle.ptr += descriptorIndex *    device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);commandList->SetGraphicsRootDescriptorTable(    2, // RootParameterIndex    &GPUDescriptorHandle); // BaseDescriptor`

Note that although the function is called `SetGraphicsRootDescriptorTable`, there is no such thing as “descriptor table” as a separate type of object. A “descriptor table” is just a sequence of 1 or more descriptors in a descriptor heap, denoted by an address of the first one.

Note also that while `D3D12_GPU_VIRTUAL_ADDRESS` was just a `typedef` to `UINT64`, `D3D12_GPU_DESCRIPTOR_HANDLE` (and `D3D12_CPU_DESCRIPTOR_HANDLE` described later) are structures that have the address as member `ptr`. It is a bit of inconsistency, but it makes sense for them to be different types so the developer doesn’t confuse them by accident.

Note also that despite similar names, `ID3D12DescriptorHeap` is not a kind of `ID3D12Heap`, but a completely separate type of object. `ID3D12Heap` is a piece of D3D12 memory intended to create “placed” resources (buffers and textures) in it, while `ID3D12DescriptorHeap `is used for storing descriptors. Flags for selecting memory type are also different. `ID3D12Heap` uses `D3D12_HEAP_TYPE_DEFAULT`, `UPLOAD` (mentioned above), or `READBACK`, while `ID3D12DescriptorHeap` just accepts `D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE` set or not set (which is described below).

We discussed the creation of the descriptor heap, as well as setting it as the root parameter. One thing that remains is actually filling the contents of the descriptor. Similarly to what we did with a root descriptor in approach B) by calling `SetGraphicsRootConstantBufferView`, a descriptor in a descriptor heap can be initialized using function `ID3D12Device::CreateConstantBufferView`.

In the following code, structure `D3D12_CONSTANT_BUFFER_VIEW_DESC` describes the parameters of the constant buffer our descriptor should point to, including its GPU virtual address and size. Second parameter of the `CreateConstantBufferView` function is the address of the descriptor that should be written. Once again, it has to be a memory address in bytes, so we need to fetch the address of the heap start and then increment it by the descriptor index multiplied by the size of a descriptor. This time, however, it is a CPU address.

`D3D12_GPU_VIRTUAL_ADDRESS constBufGPUAddr = constBuf->GetGPUVirtualAddress();D3D12_CONSTANT_BUFFER_VIEW_DESC CBVDesc = {};CBVDesc.BufferLocation = constBufGPUAddr;CBVDesc.SizeInBytes = sizeof(glm::mat4);D3D12_CPU_DESCRIPTOR_HANDLE CPUDescriptorHandle =    descriptorHeap->GetCPUDescriptorHandleForHeapStart();CPUDescriptorHandle.ptr += descriptorIndex *    device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);device->CreateConstantBufferView(&CBVDesc, CPUDescriptorHandle);`

Note that in the considerations above, we met 3 different ways of indexing descriptors inside the heap, which should not be confused:

1. When setting some descriptor as a root parameter using `SetGraphicsRootDescriptorTable`, we use an address of type `D3D12_GPU_VIRTUAL_ADDRESS` (note the “GPU” part of the address type). It is expressed in bytes, so any descriptor index has to be multiplied by the “DescriptorHandleIncrementSize”. This operation is a command recorded to a command buffer, hence it lies on the green background.
2. When filling a descriptor using `CreateConstantBufferView`, we use the address of type `D3D12_CPU_VIRTUAL_ADDRESS`. It is also expressed in bytes, so a descriptor index has to be multiplied by the same “DescriptorHandleIncrementSize” – this value is the same on CPU and GPU side. This operation happens immediately on the CPU, hence it lies on the blue background.
3. An additional offset can be applied from the descriptor we set as the root parameter to the one that will be accessed by the shader, by filling `D3D12_DESCRIPTOR_RANGE::OffsetInDescriptorsFromTableStart`. As its name says, it is expressed in descriptors, not bytes, so it doesn’t need to be multiplied by anything. In our example, we just set it to 0.

Note that “descriptorHeap” is depicted at the edge between command buffer and CPU realm, just like “constBuf”. It means that the object as a whole, as well as its descriptors that are referred by our shader, also needs to remain alive and unchanged from the moment we submit the command buffer for execution until we are sure it finished executing on the GPU. Otherwise, bad things will certainly happen. This is very important, as accessing bad descriptors is a notorious source of GPU crashes, while D3D Debug Layer cannot detect them, only GPU-based validation can!

Similarly to the constant buffer, a descriptor heap can also be set up in two different ways. A more direct way is to create just one heap with `D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE` and write to it using functions like `CreateConstantBufferView`, as in the example above. How descriptor heaps work internally is an implementation detail, but we can imagine a possible implementation of storing them in the special region of GPU memory visible to the CPU called Base Address Register (BAR). Then, the `CreateConstantBufferView` call would write data through PCI Express bus.

The other option is to create one “staging” descriptor heap without `D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE` flag that will probably end up in the system memory, another one with this flag, and do a copy between them using function `ID3D12Device::CopyDescriptorsSimple` or, less efficient and thus not recommended, `CopyDescriptors`. Note that unlike `CopyResource` function mentioned earlier, copying descriptors is a CPU operation happening immediately, not a command recorded to a command buffer, thus the loop arrow lies on the blue background.

This is all. We went through the whole diagram. Congratulations on reading that far! Let me now mention briefly how binding other types of resources differs from binding a constant buffer we described above. Obviously, the A) “32BIT_CONSTANT” variant is available only for constants. Other types of resources can only use B) or C).

With buffers and textures bound as Shader Resource View (SRV), we define them in HLSL code as global variables of type: `Texture1D`, `Texture2D`, `Texture3D`, `Texture2DMS` (for multisampled), `TextureCube`, `Texture2DArray` etc. or `StructuredBuffer`, `ByteAddressBuffer` and assign them a register like `t3`. On the CPU side, we keep them in the same descriptor heap of type `D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV`. When root parameter is of type `D3D12_ROOT_PARAMETER_TYPE_SRV`, a root descriptor can be set up using function `ID3D12GraphicsCommandList::SetGraphicsRootShaderResourceView` (textures are not supported here, only buffers). In case of using a descriptor table, a descriptor in a descriptor heap is set up using `ID3D12Device::CreateShaderResourceView`. The `D3D12_SHADER_RESOURCE_VIEW_DESC` we have to fill is more extensive than in case of constant buffers and gives an opportunity to specify a “view” into a texture limited to certain mip levels or array slices.

Buffers and textures can also be bound as Unordered Access View (UAV), which allows to read as well as write them in a shader. For this, a global variable must be defined in HLSL of type `RWTexture2D`, `RWStructuredBuffer`, `RWByteAddressBuffer` etc. with a register assigned like `u3`. On the CPU side, we keep them in the same descriptor heap of type `D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV`. When root parameter is of type `D3D12_ROOT_PARAMETER_TYPE_UAV`, a root descriptor can be set up using function `ID3D12GraphicsCommandList::SetGraphicsRootUnorderedAccessView` (again, only buffers are supported). In case of using a descriptor table, a descriptor in a descriptor heap is set up using `ID3D12Device::CreateUnorderedAccessView`.

Samplers are different, because a sampler is just a set of parameters used when sampling a texture, so there are no data that it would refer to. Everything is stored in the sampler itself, which can be compared to the parameters passed while setting up a descriptor. In a shader, you need to define a global variable of type `SamplerState` and assign it a register like `s3`. On the CPU side, there must be a separate descriptor heap created for them of type `D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER`. The size of a single sampler can also be different, so you need to fetch it using `device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER)`. Setting up a sampler is done using function `ID3D12Device::CreateSampler`. You then need to set this descriptor set with samplers, along with the one for `CBV_SRV_UAV`, to `ID3D12GraphicsCommandList::SetDescriptorHeaps` and point a root argument to a specific sampler using `SetGraphicsRootDescriptorTable`.

There is also a shortcut called “static samplers”. You can define a sampler differently, without using a descriptor heap, by filling member `D3D12_ROOT_SIGNATURE_DESC::pStaticSamplers` and `NumStaticSamplers`. Then, parameters of a sampler are “baked” into the root signature.

Finally, Render Target View (RTV) and Depth Stencil View (DSV) are special kinds of descriptors. We also need to create a descriptor heap for them, of type `D3D12_DESCRIPTOR_HEAP_TYPE_RTV` or `D3D12_DESCRIPTOR_HEAP_TYPE_DSV`, respectively, but unlike `CBV_SRV_UAV` and `SAMPLER` we don’t need to set such descriptor heap as “current” using `ID3D12GraphicsCommandList::SetDescriptorHeaps`. We can directly pass CPU addresses of these descriptors (of type `D3D12_CPU_DESCRIPTOR_HANDLE`) to functions that operate on render-target or depth-stencil textures, like `ID3D12GraphicsCommandList::ClearRenderTargetView`, `ClearDepthStencilView`, `OMSetRenderTargets`.

You can also read article “Resource binding in HLSL” in Microsoft documentation about a similar topic.