Shader Resource Layout

Introduction

Recall that Direct3D12 uses two-stage shader resource binding model. HLSL shader registers are fist mapped to descriptors in descriptor tables as defined by the root signature. Descriptors then reference actual resources in GPU memory. Shader resource layout described in this section and implemented by ShaderResourceLayoutD3D12 class defines mapping between shader resources and descriptors in descriptor tables. While the class only defines blueprint for the mapping, storage for resource references set by the application is provided by the shader resource cache (implemented by  ShaderResourceCacheD3D12 class and covered in the next section).  ShaderResourceLayoutD3D12 class may contain reference to the instance of  ShaderResourceCacheD3D12 class. In this case, shader resources may be bound through the resource layout object.

There are three use cases for shader resource layouts and shader resource caches that will be covered in details in this and the next sections:

  1. Every pipeline state object (implemented by  PipelineStateD3D12Impl class) maintains shader resource layout for every active shader stage
    • These resource layouts are not bound to a resource cache and are used as reference layouts for shader resource binding objects
  2. Every shader object (implemented by ShaderD3D12Impl class) contains shader resource layout that facilitates management of static shader resources
    • The resource layout defines artificial layout and is bound to a resource cache that actually holds references to resources set by the application
  3. Every shader resource binding object (implemented by ShaderResourceBindingD3D12Impl class) encompasses shader resource layout for every active shader stage in the parent pipeline state
    • Resource layouts are initialized by clonning reference layouts from the pipeline state object and are bound to the resource cache that holds references to resources bound by the application

Shader Resource Layout Structure

In Direct3D12, there are two types of descriptor tables that can hold shader-visible descriptors: CBV/SRV/UAV and Sampler:  Thereafter, ShaderResourceLayoutD3D12 class uses two internal structures:  CBV_SRV_UAV and Sampler that define how a shader resource is mapped to a descriptor:

Listing 1. Definition of  CBV_SRV_UAV and  Sampler structures.

CBV_SRV_UAV structure is derived from the base template class ShaderVariableD3DBase<> that implements basic functionality of IShaderVariable interface and similar to Sampler  structure, holds references to shader resource attributes and parent resource layout.

Note that both structures contain a reference to an instance of D3DShaderResourceAttribs structure defining the resource attributes, which is stored by ShaderResourcesD3D12 class. Both structures store root index of the descriptor table that contains resource descriptor. ResType_RootIndex member of CBV_SRV_UAV structure uses 3 bits to encode resource type (one of CBV, TexSRV, BufSRV, TexUAV, BufUAV, or Sampler), and 13-bits to encode the root index. Both structures also contain OffsetFromTableStart member that defines offset of the resource descriptor from the beginning of the descriptor table. For a textrue SRV, SamplerID memebr of CBV_SRV_UAV identifies ID of the sampler assigned to this texture. Note that this is ID of the sampler in the shader resource layout, not in shader resources.

ShaderResourceLayoutD3D12 class uses continuous chunk of memory to store resource layout elements, in the following order:

  1. Static CBV/SRV/UAV
  2. Mutable CBV/SRV/UAV
  3. Dynamic CBV/SRV/UAV
  4. Static Samplers
  5. Mutable Samplers
  6. Dynamic Samplers

Consider a shader that uses the following resources:

Listing 2. Exemplary shader resources .

For the shader resources above, the layout may look like shown in the figure below:

ResourceLayout

Figure 1. Shader Resource Layout and Shader Resources.

Notice the following:

  • Every element of shader resource layout references corresponding D3DShaderResourceAttribs instance stored by ShaderResourcesD3D12 class.
  • ShaderResourceLayoutD3D12 class keeps shared_ptr to  ShaderResourcesD3D12 class instance. Thus all shader resources referenced by layout elements are guaranteed to be alive while  ShaderResourceLayoutD3D12 instance is alive.
  • A number of different resource layouts can reference the same shader resources.
  • SRV_CBV_UAV::SamplerId references a sampler in ShaderResourceLayoutD3D12, while ShaderResources::SamplerId references a sampler in ShaderResources, and the two are not necessarily the same.
  • IsStaic flag in the sampler is not related to static variable type. Texture variable of any type (static, mutable and dynamic) can be assigned a static sampler.

The layout above defines the following tables in descriptor heaps:

ResourcesInHeaps2

Figure 2. Descriptor Tables defined by the Shader Resource Layout depicted in Figure 1.

Note that in the diagram above, descriptors in the descriptor heaps reference actual resources (or objects in case of samplers). Also note that in reality, mutable and static resources are placed in the same table, while single CBVs are placed into root parameters, so the layout will look little different. Nevertheless what is important now is how resource layout is related to shader resources and how ShaderResourceLayoutD3D12 defines mapping between resources and descriptors.

There are three ways shader resource layout can be initialized that correspond to three usage scenarios mentioned in the introduction. These ways are described below.

Initializing Shader Resource Layouts and Root Signature in a Pipeline State Object

In Direct3D12, pipeline state is a monolithic object that almost completely defines the state of the GPU pipeline (all bound shaders, blend state, rasterizer state, depth-stencil state, input/output formats etc.). Unlike in D3D11, in D3D12 it is not possible to change just one shader. Instead, it is necessary to create the whole new pipeline state object with this shader changed. Root signature describes how shaders in a specific pipeline state access their resources and is directly related to the shader resource layout. Pipeline state class PipelineStateD3D12Impl maintains an instance of  ShaderResourceLayoutD3D12 for every active shader stage, which are initialized together with the root signature when pipeline state object is created.

Root signature defines an array of root parameters. Every parameter is assigned a unique root index and can be one of the following: root constant, root table or root view. Every root table consists of one or more descriptor ranges. Descriptor range is a continuous range of one or more descriptors of the same type. Refer to MSDN for more details. To facilitate layout initialization, root signature class maintains growing list of root parameters that is managed by RootParamsManager internal class. The class contains two arrays: root tables and root views, which are extended as new shader resources are added. A resource may be added to the new or existing table, or added as the root view according to the following rules:

  1. Static and mutable shader variables are placed in the same root table, while dynamic variables go into a separate table
    • This implements the concept of grouping resources by the frequency of updates. Static resources never change, while mutable resources change on a per-model basis. Dynamic resources change very often and are handled separately from other resources
    • Original idea was to put static and mutable resources into separate descriptor tables, but due to the limitiation on the number of tables that may contain Shader Resource Views, they are placed in the same table
  2. CBV/SRV/UAV and Sampler variables are placed into separate tables
    • This is D3D12 requirement
  3. Resources for different shader stages are placed in separate tables
    • This is because different resources in different shader stages may be mapped to the same register
  4. Single constant buffer views are allocated directly in the root signature
    • This is more efficient
    • Constant buffer arrays are allocated in root tables similar to SRVs and UAVs

As a result, the maximum number of descriptor tables that can be allocated for one shader stage is 2 (static/mutable or dynamic) x 2 (CBV/SRV/UAV or Sampler) = 4.

RootParamsManager class provides two methods to add a new root table or a new root view:

where RootIndex is the root index of the new root table or root view, Visibility  is one of D3D12_SHADER_VISIBILITY enum and defines which shader stage can access this root parameter (right now, every parameter can be accessed by a single shader stage only). VarType is the shader variable type (static, mutable or dynamic). For AddRootTable() function, NumRangesInNewTable is the number of ranges to add to the new table. For AddRootView() function, ParamterType is one of D3D12_ROOT_PARAMETER_TYPE (for now, only  D3D12_ROOT_PARAMETER_TYPE_CBV is used), and  Register  is the shader register to which constant buffer is bound in the shader.

RootParamsManager provides methods to access existing root tables and root views:

Where RootParameter is a wrapper over D3D12_ROOT_PARAMETER type. Note that both methods take index of the root table or root view in the array of root tables or root views, but not the root index in the root signature.  Finally, RootParamsManager provides a method to add descriptor range to an existing table:

Where RootTableInd is the array index (again, not the root index) of the table where to add new ranges.

In the process of root parameter assignment, root signature class maintains two helper arrays. The first array keeps root table array index (not the root index) of a table in CBV/SRV/UAV descriptor heap, for every shader variable type and every shader stage. The second array stores the same data for Sampler heap:

Arrays are initialized with -1 indicating that initially no tables are used. When new descriptor slot is requested, the allocation routine first checks if the table is already assigned to the combination of the variable type and heap type, for that shader stage. If it is, then a new slot is added to that table. Otherwise a new table is added to the signature. Single constant buffer views are allocated directly in the root signature. Constant buffer view arrays are allocated in root tables.

Consider exemplary shader resources shown in Listing 1. Resources are processed in the order they appear in the shader, so the three constant buffers ( cbFrameAttribs, cbModelAttribs, and  cbDynamicAttribs) will be handled first. The buffers will be added as root views and will occupy root parameters at indices 0, 1, 2 (see Figure 3). Note that in Figure 2, the buffers are shown to to be placed into root tables for illustration purposes. Next resource that will be processed will be mutable texture SRV  g_DiffuseTex. To handle it, the algorithm will first check if there is a valid index in m_SrvCbvUavRootTablesMap array that corresponds to static resource (static and mutable resources are placed in the same table and handled as if both were static), for this shader stage (assuming, the stage is PS). Since there will be no table, the algorithm will add a new CBV/SRV/UAV table allocated at root index 3, which will contain single descriptor range. The range will contain single SRV descriptor corresponding to shader register t0, which is where g_DiffuseTex is bound in HLSL. Note that the array index of this table will be 0 as this is the very first table added to root parameters, but root index of this table will be 3. Texture SRVs and samplers are processed together, so the next resource to be processed will be g_DiffuseTexSampler. The algorithm again will fist look for a valid index in m_SamplerRootTablesMap array, and since there is no table for static/mutable sampler in PS, the algorithm will add a new Sampler table at root index 4. The table will contain single range that will define single resource g_DiffuseTexSampler. Notice that all root parameters added so far will have visibility  D3D12_SHADER_VISIBILITY_PIXEL. The structure of root parameters and contents of  m_SrvCbvUavRootTablesMap and  m_SamplerRootTablesMap arrays will be as shown in Figure 3.

 

RootParamsManager_B

Figure 3. Structure of the root parameters after processing cbFrameAttribs, cbModelAttribs, cbDynamicAttribs, g_DiffuseTex, and g_DiffuseTexSampler shader resources.

The source code of the function that allocates a new resource slot in the root signature is given in the listing below:

Listing 3. AllocateResourceSlot() function.

Notice that the function returns root index and descriptor offset of the allocated descriptor through output parameters RootIndex  and OffsetFromTableStart. For root views,  OffsetFromTableStart will contain invalid index. These parameters are used by the Shader Resource Layout, as we will discuss later.

Continuing the example above, the next resource will be static SRV g_ShadowMap. Since static and mutable variables are added to the same table, the algorithm will examine m_SrvCbvUavRootTablesMap array and will find that there already exists suitable root table with array index 0 (and root index 3). The shadow map will be added to this table. For that, the table will be extended to contain two ranges. The second range will define g_ShadowMap SRV that is bound to shader register t1, and corresponding descriptor will be at offset 1 from the beginning of the table. g_ShadowMap_sampler texture sampler will be handle in a similar manner, and will be placed into a descriptor table [1] (see Figure 4).

RootParamsManager2_C

Figure 4. Structure of root parameters after processing all shader resources form Listing 2. Notice that values of ShaderRegister for root views and BaseShaderRegister for descriptor ranges match registers of resources in Listing 2 and Figure 1.

As it was mentioned earlier, root signature is initialized together with the layouts for all active shader stages by the constructor of  PipelineStateD3D12Impl class. For every active shader stage, the class calls ShaderResourceLayoutD3D12::Initialize() method and provides pointer to the RootSignature class instance, that maintains growing list of root parameters. The method takes pointer to an instance of ShaderResourcesD3D12 class that stores shader resource, and goes through the following steps:

  1. Goes over all shader resources and counts total number of CBV/SRV/UAV and Sampler resources (see Listing 1 and Figure 1)
  2. Allocates raw memory to store all CBV_SRV_UAV and Sampler instances in a continuous chunk of memory
  3. Goes over all shader resources again, and for every resource:
    • Calls RootSignature::AllocateResourceSlot() to allocate the required number of descriptors in the descriptor table or allocate CBV as a root view
    • Initializes  CBV_SRV_UAV instance using the Root Index and Descriptor Offset returned by the RootSignature::AllocateResourceSlot()
    • If resource is a texture SRV, and there is a valid sampler assigned to this SRV
      • If the sampler is marked as static sampler, then
        • Calls RootSignature::InitStaticSampler() to initialize new static sampler for this shader register
          • Static samplers are not included into the layout (as opposed to samplers of static variable type)
        • Otherwise:
          • Calls RootSignature::AllocateResourceSlot() to allocate the required number of descriptors for this sampler
          • Initializes  Sampler instance using the Root Index and Descriptor Offset returned by the RootSignature::AllocateResourceSlot() for this sampler
          • Sets SamplerID member of the new  CBV_SRV_UAV instance to point to this Sampler instance

RootSignature::InitStaticSampler() function that has not been mentioned before is used by the system to initialize a new static sampler. Static samplers are part of the root signature and are not referenced through descriptors. They are not included into the shader resource layout and do not occupy space in the resource cache. Attempting to bind a sampler object to a static sampler is an error.

The function is called for every shader stage, one after another. While resources of every shader stage are processed, new parameters are added to the growing list of parameters maintained by the RootSignature class. A detailed example of this process is given in the next section. After resource layouts for all active shaders are initialized, the root signature defines all resources used by all stages. It is then finalized and D3D12 root signature object is created.

Initializing Special Resource Layout for Managing Static Shader Resources

There is one special usage case for shader resource layout: referencing static shader resources. In Diligent Engine, static resources never change once they are bound. Also, while mutable and dynamic resources are set through the shader resource binding, static resources are set directly through shaders. Thus every shader instance contains shader cache to keep references to bound static resources and an instance of ShaderResourceLayoutD3D12 class that helps manage these references. This instance defines the following artificial layout that contains four root tables:

  • Root table at index 0 stores SRV resources with descriptor offset being equal to the resource register
  • Root table at index 1 stores UAV resources with descriptor offset being equal to the resource register
  • Root table at index 2 stores CBV resources with descriptor offset being equal to the resource register
  • Root table at index 3 stores Sampler objects with descriptor offset being equal to the sampler register

For instance, for resources from Listing 2, the static resource layout will look like shown in the figure below:

StaticResourceLayout_A

Figure 5. Static resource layout for resources from Listing 2.

Notice that root indices are artificial and are defined by the shader resource type, and that offset is equal to the resource bind point.

Every ShaderD3D12Impl class contains an instance of ShaderResourceLayoutD3D12 that keeps static resource layout. It is initialized when shader object is created by the constructor of ShaderD3D12Impl. Initialization is performed also by ShaderResourceLayoutD3D12::Initialize() method, but it operates in different mode:

  1. The method goes over all shader resources, but counts only static CBV/SRV/UAV and Sampler resources
  2. It allocates raw memory to store only static resources
  3. Then goes over all static shader resources again, and for every resource:
    • Instead of allocating resource slot through the root signature (which is not available), the method allocates artificial slot as described above
    • For every texture SRV that is assigned a valid non-static sampler, the method also allocates sampler slot
      • Do not confuse static sampler with static sampler variable! Static sampler must be defined during shader creation. It is immutable and cannot be assigned any sampler object at run-time. Static sampler variable is like any other static variable. It can be assigned sampler through the shader, but cannot not be changed once it is assigned
  4. Finally, the method initializes shader resource cache
    • The cache will contain four tables corresponding to four artificial root indices. Size of each table is defined by the maximum bind point of the corresponding resource type.

For the example above, SRV and Sampler tables will contain two descriptor slots each, but only the second will be used, because static SRV g_ShadowMap as well as its corresponding Sampler g_ShadowMap_smpler are bound to registers t1 and s1 respectively, see Listing 2.

Initializing Resource Layouts in a Shader Resource Binding Object

The final way to initialize shader resource layout is to clone it from other layout. This method is used when shader resource binding object (implemented by ShaderResourceBindingD3D12Impl) is created. Shader resource binding objects are created by the pipeline state. The pipeline state initializes shader resource layout for every active shader state as described above. These layouts are cloned by the shader resource bindings. Also, shader resource binding object contains shader resource cache that is bound to all layout objects and is used to keep references to resources bound by the application.

Summary

There are three ways Diligent Engine uses Shader Resource Layouts and Shader Resource Caches:

  1. Every pipeline state initializes reference shader resource layouts for every active shader state that define how shader registers are mapped to resource descriptors. The layouts are directly related to the root signature and are not bound to a resource cache
  2. Every shader object initializes special resource layout that is bound to a resource cache to manage references to static resources
  3. When pipeline state creates a shader resource binding object, its reference resource layouts are cloned into the resource binding object and bound to a resource cache

Diagram below shows different components of the shader resource binding system.

ShaderResourceLayoutUseCases_A

Figure 6. Shader resource layouts and shader resource caches in different parts of the system.

 

Read next: Shader Resource Cache.