Shader Resource Cache

Introduction

Shader resource cache (implemented by ShaderResourceCacheD3D12 class) provides storage to keep references to objects bound by the application to shader resources. Shader resource cache is used by:

  1. the shader resource binding object ( ShaderResourceBindingD3D12Impl class) to store references to resources bound by the application
  2. the shader object ( ShaderD3D12Impl class) to keep references to static resources

Cache structure is fully defined by the shader resource layout. For every root parameter, resource cache provides space to keep references to all resources that may be bound to this parameter. A root parameter may be a root table or a root view. Root views are treated by the resource cache is single-element tables. Note that the cache does not know how resources it keeps map to the HLSL shader resources, but only provides space to keep references to them. Resources are accessed by the help of the resource layout object that knows in which descriptor table and at what offset every resource resides.

Cache Structure

Every cached resource is described by the following structure:

The structure holds type of the bound resource (one of CBV, TexSRV, BufSRV, TexUAV, BufUAV, or Sampler), CPU descriptor handle of its view in a CPU-only descriptor heap as well as strong reference to the resource to keep it alive while it is bound. Resources are grouped into tables according to the resource layout. The RootTable class describes a table and contains the following data:

m_TableStartOffset  is the offset from the beginning of the allocation in a GPU-visible descriptor heap to the first descriptor in this table. This offset is only valid for tables containing static and mutable resources. For tables containing dynamic variables as well as for single-element tables corresponding to resource views, this value is -1.   m_NumResources is the number of resources in the table, and m_pResources is the pointer to the RootTable structure containing data of the first resource in the table. Given the offset (in descriptors) from the start of the table, the resource can be accessed like shown below:

ShaderResourceCacheD3D12 class contains the following members:

The cache uses continuous chunk of memory to store tables and resources as shown in the figure below.

ResourceCacheMemLayout

The memory is allocated via  m_pAllocator, and referenced by  m_pMemory pointer. The following listing presents source code of the function that initializes storage of the cache:

Note that the function uses inplace new to construct objects at the specified memory address. Tables and resources need to be explicitly destroyed when ShaderResourceCacheD3D12 instance is destroyed and memory is released. Also note that the cache may contain zero-size tables.

Root table at the specified RootIndex can be accessed as in the code snippet below:

Specific resource in the table can the be accessed using the RootTable::GetResource() method.

Shader resource cache may be assigned space in GPU-visible descriptor heaps. In this case,  m_SamplerHeapSpace and/or m_CbvSrvUavHeapSpace member will contain non-null descriptor heap allocation in corresponding GPU Descriptor Heaps. Only static and mutable resources are assigned space in the allocation. Dynamic resources are processed separately, and descriptors for them are allocated at every draw call.

For the shader layout shown in Figure 1 in the previous section, the cache will look like shown below:

ResourceCache4

To better understand what is shown in the figure above, we need to recall what descriptors and descriptor handles are. Descriptor handle is essentially a pointer, an address in either CPU or GPU virtual address space. So descriptor handle only points to the handle, which describes resource in a hardware-specific format. The descriptor itself is located in a descriptor heap that is always CPU-visible, but may also be GPU-visible. Every descriptor has CPU descriptor handle, which is its address in CPU address space. GPU-visible descriptors also have GPU descriptor handle, which is descriptor address in GPU virtual address space. CPUDescriptorHandle member of Resource structure is a handle of the resource descriptor in CPU-only descriptor heap. This descriptor is allocated by CPUDescriptorHeap class when resource view is created. When a resource is bound to the shader variable, the handle (i.e. pointer) is stored in Resource::CPUDescriptorHandle. CPU-only descriptor cannot be used by the draw commands because it is not visible to the GPU. Thus resource cache can be assigned space in GPU-visible descriptor heaps. If this is the case, descriptor (not just the handle, but the descriptor itself) is also copied to the appropriate location in a shader-visible heap. As a result, there may be two copies of a descriptor (not just descriptor handle!) that reference the same resource (as shown in the figure above). Resource::CPUDescriptorHandle points to one copy in the CPU-only heap, while second copy resides in the GPU-visible heap. CPU handle of the shader-visible descriptor (that is, its address in CPU virtual address space) of the resource in the specified root table at the given offset is returned by the following function:

Note that for the sake of efficiency the function is a template with the template argument being type of the descriptor heap. GPU descriptor handle for the same resource is given by similar function, that uses DescriptorHeapAllocation::GetGpuHandle().

Notice one important detail: descriptor table at root index 2 is not assigned space in shader-visible allocation, so its table start offset is -1. This table contains dynamic CBV/SRV/UAV resources ( cbDynamicAttribs in this particular example). Descriptor handles for dynamic resources are allocated at every draw call, as described in section on GPU Descriptor Heap.

Initializing the Cache

Shader resource cache can be initialized using two different methods that correspond to two usage scenarios, as described below.

Initializing the Cache for Shader Resource Binding Object

In a shader resource binding object ( ShaderResourceBindingD3D12Impl class), the shader resource cache is used to keep references to all resources bound by the application, for shader variables of all types (static, mutable, and dynamic). The cache also references space in CBV/SRV/UAV and Sampler descriptor heap to store descriptors of static and mutable resources. The cache does not provide space to store dynamic resource descriptors, which are allocated by the system for every draw call. Shader resource cache is initialized by the Root Signature, because only the root signature has enough information to initialize the cache that will suit all shader stages. Every shader resource layout only knows about resources of one shader stage and has no information about other stages.

The following listing gives source code of the RootSignature::InitResourceCache() function that initializes the resource cache:

The RootSignature::InitResourceCache() takes the following steps:

  1. Populates CacheTableSizes array that contains root table size, for every root index
  2. Allocates memory for all root tables
  3. Computes total number of static and mutable descriptors in CBV/SRV/UAV and Sampler tables and allocates space in GPU-visible descriptor heaps
  4. Iterates through all descriptor tables and assigns offset to tables containing static and mutable variables. Tables are tightly packed, so offset of table N+1 is always equal to offset of table N plus its size
    • Offsets are not assigned to tables containing dynamic resources and root views, though the cache still holds references to objects
  5. Passes GPU-visible descriptor heap allocations over to the resource cache

Initializing the Cache for Static Shader Resources

As previous section described, static shader resources are managed using special layout:

  • 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 resources with descriptor offset being equal to the resource register

The layout is initialized by the ShaderResourceLayoutD3D12::Initialize() method, which also initializes the resource cache. The size of every table is defined by the maximum bind point for every resource type, which the function computes while processing the resources.

Static resource cache is not assigned space in GPU-visible descriptor heaps.

The Big Picture

To illustrate how shader resource layout, shader resource cache, root signature, shader resource binding, and other objects interact with each other, let’s consider a pipeline state that contains two shaders, a vertex shader with the following shader resources:

and a pixel shader with the following resources:

Initializing Shader Objects

The first step is creating two ShaderD3D12Impl objects. During initialization, the objects will enumerate their shader resources. Vertex shader resource will look like shown in the figure below:

VS_Resources

Pixel shader resources are shown in the following figure:

PS_Resources

 

Notice that dynamic texture g_DynamicLight is assigned a static sampler g_DynamicLight_sampler, which is perfectly valid.

Both shader objects will initialize special shader resource layouts and shader resource cache to manage references to objects bound to static resources. For the vertex shader, the static resource layout will define two tables: a table at root index 0 to store object bound to the texture SRV g_HeightMap, and a table at root index 2 to store object bound to constant buffer cbFrameAttribs. The cache will be initialized to provide storage for these two tables as show in the figure below:

VS_Static_Resources_F

Note that static sampler g_HeightMap_sampler is not referenced by the resource layout and does not have space in the resource cache.

For the pixel shader, the static shader resource layout will also define two tables: the first one at root 0 will contain two slots. The reason is that the offset in the descriptor table is defined by the resource register, which is for texture SRV  g_ShadowMap is 1. Slot 0 will be unused. To avoid waste of space in the cache it is a good idea to define static resources first in the HLSL file. The second table at root index 2 will contain one slot to store object bound to constant buffer cbLightAttribs. The layout and the cache will look like shown in the figure below:

PS_Static_Resources_C

Note again that static sampler g_ShadowMap_sampler is not referenced by the resource layout and does not have space in the resource cache. Also note that since static resource caches are not assigned space in GPU-visible descriptor heaps, all TableStartOffset values in root tables are set to -1.

Initializing Shader Resource Layouts in a Pipeline State

After shader objects are initialized, pipeline state object can be created. As previous section described, constructor of PipelineStateD3D12Impl class will initialize shader resource layout for every shader stage together with the root signature. This process will proceed as follows. At first, vertex shader resources will be processed in the order of declaration in HLSL file:

  1. cbFrameAttribs constant buffer will be placed into a root view at root index 0 (root parameter visibility D3D12_SHADER_VISIBILITY_VERTEX)
  2. cbModelAttribs constant buffer will be placed into a root view at root index 1 (visibility D3D12_SHADER_VISIBILITY_VERTEX)
  3. g_HeightMap texture SRV will be placed into a root table at index 2 (visibility D3D12_SHADER_VISIBILITY_VERTEX) into the first slot
    • Since  g_HeightMap_sampler is a static sampler (not sampler of static variable type), it will not be added to the resource layout, so SamplerId  member will be -1
  4. g_VertexData buffer SRV is mutable resource. It will be added into the same table as static texture SRV g_HeightMap, into the second slot

Next, pixel shader resources will be processed. The resources will be added to the growing list of root parameters maintained by the root signature:

  1. cbLightAttribs constant buffer will be placed into a root view at the next available root index, which is 3 (root parameter visibility D3D12_SHADER_VISIBILITY_PIXEL)
  2. g_DiffuseTex mutable texture SRV will be placed into a new root table at root index 4 (visibility D3D12_SHADER_VISIBILITY_PIXEL), at offset 0
    • g_DiffuseTex is assigned a non-static texture sampler g_DiffuseTex_sampler, which will be processed next. Since sampler descriptors must reside in a separate descriptor heap, the sampler will be placed into a new root table at root index 5 (visibility D3D12_SHADER_VISIBILITY_PIXEL), at offset 0
  3. g_ShadowMap static texture SRV will be added to the same table as g_DiffuseTex mutable texture SRV (at root index 4), at offset 1
    • Since  g_ShadowMap_sampler is a static sampler, it will not be included into the layout
  4. g_DynamicLight dynamic texture SRV will be placed into a new table (because dynamic resources are handled separately from static and mutable ones), at offset 0
    • g_DynamicLight_sampler is a static sampler (it is perfectly valid to assign static sampler to a texture SRV of any type), and it will not be included into the layout

After processing all resources, the shader layouts will look like shown in the figure below:

ResourceLayoutInPSO_B

Initializing Shader Resource Cache for a Shader Resource Binding Object

When shader resource binding object is created from the pipeline state, the resource layouts are cloned and bound to the resource cache. The cache will be initialized by the root signature as discussed above. It will contain 7 root tables that correspond to 7 allocated root parameters. The cache will be assigned space in GPU-visible descriptor heaps: four descriptors in a CBV/SRV/UAV descriptor heap to store 4 static/mutable SRVs ( g_HeightMap, g_VertexData, g_DiffuseTex, g_ShadowMap), and one descriptor in a Sampler descriptor heap to store mutable sampler g_DiffuseTex_sampler. The figure below illustrates the structure of the resource cache:

ResourceCacheInSRB

Notice the following:

  • Root tables at root indices 0,1 and 3 are not assigned valid table start offset because corresponding parameters are root views, and the tables only keep references to buffer objects bound as CBVs
  • All mutable/static CBV/SRV/UAV resources of both shaders are tightly packed into a continuous descriptor heap allocation. The table for the vertex shader descriptors starts at offset 0, the table for the pixel shader descriptors starts at offset 2
  • There is only one descriptor in GPU-visible sampler descriptor heap that is used to store mutable descriptor g_DiffuseTexSampler
  • Root table at index 6 is not assigned a valid offset. This table references descriptor of a dynamic texture SRV g_DynamicLight, which is allocated at every draw call from the dynamic part of GPU descriptor heap

Binding Objects to Shader Variables

As it was noted, shader resource cache knows very little about resources it holds. Shader resource layout, to the contrary, has all information required to identify resource in the cache (its root index, offset in the table, and array index for array resources). So binding object to shader resource variable is performed through the shader resource layout. ShaderResourceLayoutD3D12::SRV_CBV_UAV  structure inherits IShaderVariable interface and implements IShaderVariable::Set() method that binds an object to the shader variable. Every  SRV_CBV_UAV instance keeps reference to parent  ShaderResourceLayoutD3D12 object, which in turn keeps pointer to the shader resource cache. Resource binding is implemented by the ShaderResourceLayoutD3D12::SRV_CBV_UAV::BindResource() method. The first step is to get reference to the right resource slot in the resource cache:

If the cache contains allocation in a GPU-visible descriptor heap, we will need CPU descriptor handle in this allocation:

The BindResource() method then passes reference DstRes as well as descriptor handle ShdrVisibleHeapCPUDescriptorHandle to a specific method depending on the type of the resource. All methods do mostly similar actions with minor variations. For instance, for a constant buffer, the method calls ShaderResourceLayoutD3D12::SRV_CBV_UAV::CacheCB() which is given in the listing below:

 

As you can see, if CPU descriptor handle of a GPU-visible descriptor heap is not null, the method copies descriptor to this heap. It also stores strong reference to the buffer object to make sure the resource is alive while it is bound.

Summary

Shader resource cache is the final part of shader resource binding system. It provides storage to keep references to objects bound to shader resource variables. Shader resource cache does not know about the structure of resources it keeps, thus binding resources to cache is performed through the shader resource layout object.

 

Read next: binding resources to the GPU pipeline.