Resource Binding in Microsoft DirectX 12

Original author: Wolfgang Engel
  • Transfer

On March 20, 2014, Microsoft announced at the Game Developers Conference the release of DirectX * 12. By reducing the excessive processing of resources, DirectX 12 will contribute to more efficient application operation and lower power consumption, so you can play on mobile devices longer without recharging.
Intel experts measured at SIGGRAPH 2014CPU power consumption when starting a simple demo with asteroids on a Microsoft Surface * Pro 3 tablet. You can switch the demo application from the DirectX 11 API to the DirectX 12 API by pressing a button. This demo application draws a huge number of asteroids in space at a fixed frame rate. When using the DirectX 12 API, the CPU power consumption is more than halved compared to DirectX 11 **. The device operates in less intense thermal conditions and is able to work longer on battery power. In typical game scenarios, all the unused CPU power can be spent on improving physics, artificial intelligence, path-finding algorithms, or other tasks with an intense CPU load. Thus,

Instruments


To develop games based on DirectX 12, the following is required.
  • Windows * 10
  • DirectX 12 SDK
  • Visual Studio * 2013
  • GPU drivers supporting DirectX 12

If you are a game developer, try participating in the Microsoft DirectX Early Access Program .
After accepting the DirectX Early Access Program, you will receive SDK installation instructions and drivers for the GP.

Overview


From a high-level point of view, compared to DirectX 10 and DirectX 11, DirectX 12 architecture differs in the field of state management, tracking and resource management in memory.
DirectX 10 introduced state objects to configure a group of states at runtime. DirectX 12 introduces state pipeline objects (PSOs), which serve as even larger state objects along with shaders. This article discusses changes in working with resources; the grouping of states in the PSO will be described in future articles.
In DirectX 11, the system was responsible for predicting and tracking resource usage, which limited the ability to create applications for large-scale use of DirectX 11. In DirectX 12, it is the programmer (not the system and not the driver) that is responsible for processing the following three usage models.

1. Resource Binding
DirectX 10 and 11 monitored the resource binding to the graphics pipeline to keep the resources already released by the application operational, since incomplete GP operations could reference those resources. In DirectX 12, the system does not track resource bindings. An application, that is, a programmer, should be involved in managing the life cycle of objects.

2. Resource Binding Analysis
DirectX 12 does not track resource bindings to determine if a resource switch has occurred. For example, an application can write to a render target using the render target (RTV) representation, and then read that render target as a texture using the shader resource view (SRV). In the DirectX 11 API, the GPU driver had to “know” when such a switching of resources took place in order to avoid conflicts when reading, changing, and writing data to memory. In DirectX 12, you must identify and track all resource transfers using separate API calls.

3. Synchronization of mapped memory
In DirectX 11, the driver handles the synchronization of mapped memory between the CPU and GPU. The system analyzed resource bindings to see if a rendering delay was required, since the mapping of the resource that was mapped for CPU access had not yet been canceled. In DirectX 12, an application must handle the synchronization of CPU and GPU access to resources. A single mechanism for synchronizing memory access requests an event to wake up a thread when processing is completed in the GPU.
Moving these resource utilization patterns into applications has required a new set of programming interfaces that can support a wide range of GPU architectures.
The rest of this article describes new resource binding mechanisms, the first of which are descriptors.

Descriptors


Descriptors describe the resources stored in memory. A descriptor is a data block describing an object for a GP in an “opaque” format intended for a GP. Descriptors with a slight stretch can be considered as a replacement for the old system of "representations" in DirectX 11. In addition to various types of DirectX 11 descriptors, such as shader resource representations (SRV) and disordered access representations (UAV), other types of descriptors appeared in DirectX 12, for example, samplers and persistent buffer representation (CBV).
For example, SRV chooses which base resource to use, which set of bump maps and slices of the array, and in what format to interpret the memory. The SRV descriptor must contain the virtual address of the Direct3D * resource (which may be a texture) in the GPU. The application must make sure that the underlying resource is not destroyed and is not inaccessible due to non-residency.
In fig. 1 shows a handle to the "representation" of the texture.


Figure 1. Shader resource representation in the descriptor [used with permission from Microsoft Corporation]

To create a shader resource representation in DirectX 12, use the following Direct3D device structure and method.

typedef struct D3D12_SHADER_RESOURCE_VIEW_DESC
{
    DXGI_FORMAT Format;
    D3D12_SRV_DIMENSION ViewDimension;
    union
    {
        D3D12_BUFFER_SRV Buffer;
        D3D12_TEX1D_SRV Texture1D;
        D3D12_TEX1D_ARRAY_SRV Texture1DArray;
        D3D12_TEX2D_SRV Texture2D;
        D3D12_TEX2D_ARRAY_SRV Texture2DArray;
        D3D12_TEX2DMS_SRV Texture2DMS;
        D3D12_TEX2DMS_ARRAY_SRV Texture2DMSArray;
        D3D12_TEX3D_SRV Texture3D;
        D3D12_TEXCUBE_SRV TextureCube;
        D3D12_TEXCUBE_ARRAY_SRV TextureCubeArray;
        D3D12_BUFFEREX_SRV BufferEx;
    };
} D3D12_SHADER_RESOURCE_VIEW_DESC;
interface ID3D12Device
{
...
    void CreateShaderResourceView (
        _In_opt_ ID3D12Resource* pResource,
        _In_opt_ const D3D12_SHADER_RESOURCE_VIEW_DESC* pDesc,
        _In_ D3D12_CPU_DESCRIPTOR_HANDLE DestDescriptor);
};


An example SRV code might look something like this.

// create SRV
D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc;
ZeroMemory(&srvDesc, sizeof(D3D12_SHADER_RESOURCE_VIEW_DESC));
srvDesc.Format = mTexture->Format;
srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
srvDesc.Texture2D.MipLevels = 1;
mDevice->CreateShaderResourceView(mTexture.Get(), &srvDesc, mCbvSrvDescriptorHeap->GetCPUDescriptorHandleForHeapStart());


This code creates an SRV for the two-dimensional texture and indicates its format and virtual GPU address. The final argument for CreateShaderResourceView is the handle of the handle heap that was allocated before calling this method. Descriptors are usually stored in heaps of descriptors, which are described in more detail in the next section.
Note. You can also transfer some types of descriptors to the GPU using the so-called root parameters (taking into account the driver versions). See below for more details.

Descriptor heaps


A bunch of descriptors can be thought of as one allocated amount of memory for multiple descriptors. Different types of heaps may contain one or more types of descriptors. The following types are currently supported.

Typedef enum D3D12_DESCRIPTOR_HEAP_TYPE
{
 D3D12_CBV_SRV_UAV_DESCRIPTOR_HEAP	= 0,
 D3D12_SAMPLER_DESCRIPTOR_HEAP = (D3D12_CBV_SRV_UAV_DESCRIPTOR_HEAP + 1) ,
 D3D12_RTV_DESCRIPTOR_HEAP	= ( D3D12_SAMPLER_DESCRIPTOR_HEAP + 1 ) ,
 D3D12_DSV_DESCRIPTOR_HEAP	= ( D3D12_RTV_DESCRIPTOR_HEAP + 1 ) ,
 D3D12_NUM_DESCRIPTOR_HEAP_TYPES = ( D3D12_DSV_DESCRIPTOR_HEAP + 1 ) 
} 	D3D12_DESCRIPTOR_HEAP_TYPE;


There are heap types for CBV, SRV, and UAV descriptors. There are also types for working with rendering target views (RTV) and depth format views (DSV).
The following code creates a bunch of descriptors for nine descriptors, each of which can be of type CBV, SRV, or UAV.

// create shader resource view and constant buffer view descriptor heap
D3D12_DESCRIPTOR_HEAP_DESC descHeapCbvSrv = {};
descHeapCbvSrv.NumDescriptors = 9;
descHeapCbvSrv.Type = D3D12_CBV_SRV_UAV_DESCRIPTOR_HEAP;
descHeapCbvSrv.Flags = D3D12_DESCRIPTOR_HEAP_SHADER_VISIBLE;
ThrowIfFailed(mDevice->CreateDescriptorHeap(&descHeapCbvSrv, __uuidof(ID3D12DescriptorHeap), (void**)&mCbvSrvDescriptorHeap));


The first two entries in the heap description are the number of descriptors and the types of descriptors that may be in this heap. The third parameter, D3D12_DESCRIPTOR_HEAP_SHADER_VISIBLE, describes this bunch of descriptors as visible to the shader. You can use heap descriptors that are invisible to the shader, for example, for intermediate storage of descriptors in the CPU or for RTVs that are not accessible from within the shaders.
This code sets a flag, due to which a bunch of descriptors becomes visible to the shader, but there is another level of indirect addressing. A shader can “see” a bunch of descriptors through a descriptor table (there are also root descriptors that do not use tables; see below for more details).

Descriptor tables


The main goal of the descriptor heap is to allocate the necessary amount of memory to store all the descriptors for rendering in the greatest possible amount, say for one frame or more
Note. When switching between heaps of descriptors, the GP pipeline can be cleaned, depending on the equipment used. Therefore, it is necessary to minimize the operations of switching between heaps of descriptors or combine them with other operations, in which the pipeline is still cleaned up.

The descriptor table points to a bunch of descriptors using offset. Instead of forcing the graphics pipeline to always look at the whole bunch, switching descriptor tables will allow you to change the set of resources used by this shader without significant costs. In this case, the shader does not have to find resources in the heap space.
In other words, an application can use several descriptor tables for different shaders pointing to the same heap, as shown in Fig. 2.


Figure 2. Various shaders point to a bunch of descriptors using several descriptor tables.

The following code example creates descriptor tables for the SRV and sampler that are visible to the pixel shader.

// define descriptor tables for a SRV and a sampler for pixel shaders
D3D12_DESCRIPTOR_RANGE descRange[2];
descRange[0].Init(D3D12_DESCRIPTOR_RANGE_SRV, 1, 0);
descRange[1].Init(D3D12_DESCRIPTOR_RANGE_SAMPLER, 1, 0);
D3D12_ROOT_PARAMETER rootParameters[2];
rootParameters[0].InitAsDescriptorTable(1, &descRange[0], D3D12_SHADER_VISIBILITY_PIXEL);
rootParameters[1].InitAsDescriptorTable(1, &descRange[1], D3D12_SHADER_VISIBILITY_PIXEL);


At the same time, the descriptor table is visible only to the pixel shader; this restriction is set using the D3D12_SHADER_VISIBILITY_PIXEL flag . The following listing defines the various levels of visibility of the descriptor table.

typedef enum D3D12_SHADER_VISIBILITY
{
 D3D12_SHADER_VISIBILITY_ALL	= 0,
 D3D12_SHADER_VISIBILITY_VERTEX	= 1,
 D3D12_SHADER_VISIBILITY_HULL	= 2,
 D3D12_SHADER_VISIBILITY_DOMAIN	= 3,
 D3D12_SHADER_VISIBILITY_GEOMETRY	= 4,
 D3D12_SHADER_VISIBILITY_PIXEL	= 5
} D3D12_SHADER_VISIBILITY;


If you specify a flag that sets the visibility for all, the arguments will be passed to all stages of the shader, although visibility is set only once.
A shader can discover resources using descriptor tables, but first the shader must “learn” about these descriptor tables using the root parameter in the root signature.

Root signature and parameters


The root signature stores the root parameters used by shaders to locate resources that need access. These parameters exist in the form of a binding space in the list of commands for a set of resources that the application must make available for shaders.
The root arguments may be as follows.
  • Descriptor tables. As described above, they contain the offset and number of descriptors in the heap.
  • Root descriptors. Only a small number of descriptors can be stored directly in the root parameter. In this case, the application no longer needs to place these descriptors in the heap of descriptors, indirect addressing is eliminated.
  • Root constants. These are constants provided directly to shaders, without the need to work with root descriptors and descriptor tables.

To achieve optimal performance, applications typically sort the root parameters in descending order of frequency.
All root parameters, such as descriptor tables, root descriptors and root constants, are combined into a list of commands, and the driver will manage their versions on behalf of the application. In other words, whenever any of the root parameters changes between render or send calls, the equipment will update the version number of the root signature. When changing any argument, each render or send call receives a unique complete set of root parameter states.
Root descriptors and root constants reduce the level of indirect GP addressing upon access; descriptor tables allow you to access larger amounts of data, but the level of indirect addressing increases. Due to the higher level of indirect addressing when using descriptor tables, an application can initialize the content until a list of commands is sent for execution. In addition, the 5.1 shader model, supported by all DirectX 12 hardware, allows shaders to dynamically index all specified descriptor tables. Therefore, the shader can select the desired descriptor from the descriptor table during shader execution. An application can simply create one large descriptor table and always use indexing (for example, using the material identifier) ​​to get the desired descriptor.

The performance of different architectures can vary when using large sets of root constants and root descriptors compared to using descriptor tables. For this reason, you need to optimally configure the relationship between root parameters and descriptor tables depending on the target hardware platforms.
A perfectly balanced application can use a combination of all types of bindings: root constants, root descriptors, descriptor tables for descriptors received on the fly as rendering calls are issued, and dynamic indexing of large descriptor tables.

In the following code, the two descriptor tables mentioned above are stored as root parameters in the root signature.

// define descriptor tables for a SRV and a sampler for pixel shaders
D3D12_DESCRIPTOR_RANGE descRange[2];
descRange[0].Init(D3D12_DESCRIPTOR_RANGE_SRV, 1, 0);
descRange[1].Init(D3D12_DESCRIPTOR_RANGE_SAMPLER, 1, 0);
D3D12_ROOT_PARAMETER rootParameters[2];
rootParameters[0].InitAsDescriptorTable(1, &descRange[0], D3D12_SHADER_VISIBILITY_PIXEL);
rootParameters[1].InitAsDescriptorTable(1, &descRange[1], D3D12_SHADER_VISIBILITY_PIXEL);
// store the descriptor tables int the root signature
D3D12_ROOT_SIGNATURE descRootSignature;
descRootSignature.Init(2, rootParameters, 0);
ComPtr pOutBlob;
ComPtr pErrorBlob;
ThrowIfFailed(D3D12SerializeRootSignature(&descRootSignature,   
              D3D_ROOT_SIGNATURE_V1, pOutBlob.GetAddressOf(),                    
              pErrorBlob.GetAddressOf()));
ThrowIfFailed(mDevice->CreateRootSignature(pOutBlob->GetBufferPointer(), 
              pOutBlob->GetBufferSize(), __uuidof(ID3D12RootSignature),                  
             (void**)&mRootSignature));

All shaders in the PSO must be compatible with the root signature specified with this PSO; otherwise, the PSO will not be created.
The root signature must be specified for a list of commands or a package. To do this, we call:

1 commandList-> SetGraphicsRootSignature (mRootSignature);
After setting the root signature, you need to define a set of bindings. In the above example, this is done using the following code.

// set the two descriptor tables to index into the descriptor heap 
// for the SRV and the sampler
commandList->SetGraphicsRootDescriptorTable(0, 
               mCbvSrvDescriptorHeap->GetGPUDescriptorHandleForHeapStart());
commandList->SetGraphicsRootDescriptorTable(1, 
               mSamplerDescriptorHeap->GetGPUDescriptorHandleForHeapStart());

The application must set the appropriate parameters in each of the two cells of the root signature before issuing a render call or a send call. For example, in the first cell there is now a descriptor marker that matches a bunch of descriptors with an SRV descriptor by index, and in the second cell there is a descriptor table that matches a bunch of descriptors with a sample descriptor in index.
An application can change, for example, the binding of a second cell in the interval between rendering calls. This means that a second render call only needs to snap the second cell.

Putting the components together


The large code snippet below shows all the mechanisms used to bind resources. This application uses only one texture, and this code provides a sampler and SRV for this texture.

// define descriptor tables for a SRV and a sampler for pixel shaders
D3D12_DESCRIPTOR_RANGE descRange[2];
descRange[0].Init(D3D12_DESCRIPTOR_RANGE_SRV, 1, 0);
descRange[1].Init(D3D12_DESCRIPTOR_RANGE_SAMPLER, 1, 0);
D3D12_ROOT_PARAMETER rootParameters[2];
rootParameters[0].InitAsDescriptorTable(1, &descRange[0], D3D12_SHADER_VISIBILITY_PIXEL);
rootParameters[1].InitAsDescriptorTable(1, &descRange[1], D3D12_SHADER_VISIBILITY_PIXEL);
// store the descriptor tables in the root signature
D3D12_ROOT_SIGNATURE descRootSignature;
descRootSignature.Init(2, rootParameters, 0);
ComPtr pOutBlob;
ComPtr pErrorBlob;
ThrowIfFailed(D3D12SerializeRootSignature(&descRootSignature,   
              D3D_ROOT_SIGNATURE_V1, pOutBlob.GetAddressOf(),                    
              pErrorBlob.GetAddressOf()));
ThrowIfFailed(mDevice->CreateRootSignature(pOutBlob->GetBufferPointer(), 
              pOutBlob->GetBufferSize(), __uuidof(ID3D12RootSignature),                  
             (void**)&mRootSignature));
// create descriptor heap for shader resource view
D3D12_DESCRIPTOR_HEAP_DESC descHeapCbvSrv = {};
descHeapCbvSrv.NumDescriptors = 1; // for SRV
descHeapCbvSrv.Type = D3D12_CBV_SRV_UAV_DESCRIPTOR_HEAP;
descHeapCbvSrv.Flags = D3D12_DESCRIPTOR_HEAP_SHADER_VISIBLE;
ThrowIfFailed(mDevice->CreateDescriptorHeap(&descHeapCbvSrv, __uuidof(ID3D12DescriptorHeap), (void**)&mCbvSrvDescriptorHeap));
// create sampler descriptor heap
D3D12_DESCRIPTOR_HEAP_DESC descHeapSampler = {};
descHeapSampler.NumDescriptors = 1;
descHeapSampler.Type = D3D12_SAMPLER_DESCRIPTOR_HEAP;
descHeapSampler.Flags = D3D12_DESCRIPTOR_HEAP_SHADER_VISIBLE;
ThrowIfFailed(mDevice->CreateDescriptorHeap(&descHeapSampler, __uuidof(ID3D12DescriptorHeap), (void**)&mSamplerDescriptorHeap));
// skip the code that uploads the texture data into heap
// create sampler descriptor in the sample descriptor heap
D3D12_SAMPLER_DESC samplerDesc;
ZeroMemory(&samplerDesc, sizeof(D3D12_SAMPLER_DESC));
samplerDesc.Filter = D3D12_FILTER_MIN_MAG_MIP_LINEAR;
samplerDesc.AddressU = D3D12_TEXTURE_ADDRESS_WRAP;
samplerDesc.AddressV = D3D12_TEXTURE_ADDRESS_WRAP;
samplerDesc.AddressW = D3D12_TEXTURE_ADDRESS_WRAP;
samplerDesc.MinLOD = 0;
samplerDesc.MaxLOD = D3D11_FLOAT32_MAX;
samplerDesc.MipLODBias = 0.0f;
samplerDesc.MaxAnisotropy = 1;
samplerDesc.ComparisonFunc = D3D12_COMPARISON_ALWAYS;
mDevice->CreateSampler(&samplerDesc, 
           mSamplerDescriptorHeap->GetCPUDescriptorHandleForHeapStart());
// create SRV descriptor in the SRV descriptor heap
D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc;
ZeroMemory(&srvDesc, sizeof(D3D12_SHADER_RESOURCE_VIEW_DESC));
srvDesc.Format = SampleAssets::Textures->Format;
srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
srvDesc.Texture2D.MipLevels = 1;
mDevice->CreateShaderResourceView(mTexture.Get(), &srvDesc,            
            mCbvSrvDescriptorHeap->GetCPUDescriptorHandleForHeapStart());
// writing into the command list
// set the root signature
commandList->SetGraphicsRootSignature(mRootSignature);
// other commands here ...
// set the two descriptor tables to index into the descriptor heap 
// for the SRV and the sampler
commandList->SetGraphicsRootDescriptorTable(0, 
               mCbvSrvDescriptorHeap->GetGPUDescriptorHandleForHeapStart());
commandList->SetGraphicsRootDescriptorTable(1, 
               mSamplerDescriptorHeap->GetGPUDescriptorHandleForHeapStart());


Static Samplers


So, we saw how to create a sampler using a bunch of descriptors and a descriptor table. But there is another way to use samplers in the application. Since many applications require only a limited set of samplers, you can use static samplers as the root argument.
Currently, the root signature is as follows.

typedef struct D3D12_ROOT_SIGNATURE
{
    UINT NumParameters;
    const D3D12_ROOT_PARAMETER* pParameters;
    UINT NumStaticSamplers;
    const D3D12_STATIC_SAMPLER* pStaticSamplers;
    D3D12_ROOT_SIGNATURE_FLAGS Flags;
    // Initialize struct
    void Init(
        UINT numParameters,
        const D3D12_ROOT_PARAMETER* _pParameters,
        UINT numStaticSamplers = 0,
        const D3D12_STATIC_SAMPLER* _pStaticSamplers = NULL,
        D3D12_ROOT_SIGNATURE_FLAGS flags = D3D12_ROOT_SIGNATURE_NONE)
    {
        NumParameters = numParameters;
        pParameters = _pParameters;
        NumStaticSamplers = numStaticSamplers;
        pStaticSamplers = _pStaticSamplers;
        Flags = flags;
    }
    D3D12_ROOT_SIGNATURE() { Init(0,NULL,0,NULL,D3D12_ROOT_SIGNATURE_NONE);}
    D3D12_ROOT_SIGNATURE(
        UINT numParameters,
        const D3D12_ROOT_PARAMETER* _pParameters,
        UINT numStaticSamplers = 0,
        const D3D12_STATIC_SAMPLER* _pStaticSamplers = NULL,
        D3D12_ROOT_SIGNATURE_FLAGS flags = D3D12_ROOT_SIGNATURE_NONE)
    {
        Init(numParameters, _pParameters, numStaticSamplers, _pStaticSamplers, flags);
    }
} D3D12_ROOT_SIGNATURE;


A set of static samplers can be defined independently of the root parameters in the root signature. As mentioned above, the root parameters define a binding space where arguments can be provided at run time, while static samplers are unchanged by definition.
Since root signatures can be created in HLSL, you can create static samplers in the same place. Currently, the application can have no more than 2032 unique static samplers. This is slightly less than the next power of two, and allows drivers to use some space for internal use.
Static samplers defined in the root signature are independent of the samplers selected by the application to place on the heap descriptors, so both mechanisms can be used simultaneously.

If the selection of the samplers is completely dynamic and unknown at the time the shader was compiled, the application must manage the samplers in the heap of descriptors.

Conclusion


DirectX 12 supports full control over resource usage patterns. The application developer is responsible for allocating memory in the descriptor heaps, for describing the resources in the descriptors, and for addressing the shader in the index to the heap descriptors through descriptor tables, which, in turn, are “expanded” for the shader using root signatures.
Moreover, using root signatures, you can define a custom parameter space for shaders using the four following types of components in any combination:
  • root constants;
  • Static Samplers
  • root descriptors;
  • descriptor tables.

The task is to select the desired form of binding for the corresponding types of resources and the frequency of their updates.

Links and useful materials



Also popular now: