Diligent Graphics > Diligent Engine > Architecture > D3D12 > Shader Resource Cache
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:
- the shader resource binding object ( ShaderResourceBindingD3D12Impl class) to store references to resources bound by the application
- 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:
1 2 3 4 5 6 7 8 |
struct Resource { CachedResourceType Type = CachedResourceType::Unknown; // CPU descriptor handle of a cached resource in a CPU-only descriptor heap // Note that for dynamic resources, this is the only available CPU descriptor handle D3D12_CPU_DESCRIPTOR_HANDLE CPUDescriptorHandle = {0}; RefCntAutoPtr<IDeviceObject> pObject; }; |
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:
1 2 3 4 5 6 7 8 9 10 |
class RootTable { // ... // Offset from the start of the descriptor heap allocation // to the start of the table Uint32 m_TableStartOffset = InvalidDescriptorOffset; const Uint32 m_NumResources = 0; // ... Resource* const m_pResources = nullptr; }; |
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:
1 2 3 4 |
Resource& RootTable::GetResource(Uint32 OffsetFromTableStart) { return m_pResources[OffsetFromTableStart]; } |
ShaderResourceCacheD3D12 class contains the following members:
1 2 3 4 5 6 7 8 9 |
// Allocation in a GPU-visible sampler descriptor heap DescriptorHeapAllocation m_SamplerHeapSpace; // Allocation in a GPU-visible CBV/SRV/UAV descriptor heap DescriptorHeapAllocation m_CbvSrvUavHeapSpace; IMemoryAllocator *m_pAllocator=nullptr; void *m_pMemory = nullptr; Uint32 m_NumTables = 0; |
The cache uses continuous chunk of memory to store tables and resources as shown in the figure below.
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
void ShaderResourceCacheD3D12::Initialize(IMemoryAllocator &MemAllocator, Uint32 NumTables, Uint32 TableSizes[]) { m_pAllocator = &MemAllocator; m_NumTables = NumTables; Uint32 TotalResources = 0; for(Uint32 t=0; t < NumTables; ++t) TotalResources += TableSizes[t]; auto MemorySize = NumTables * sizeof(RootTable) + TotalResources * sizeof(Resource); if(MemorySize > 0) { m_pMemory = ALLOCATE( *m_pAllocator, "Memory for shader resource cache data", MemorySize); auto *pTables = reinterpret_cast<RootTable*>(m_pMemory); auto *pCurrResPtr = reinterpret_cast<Resource*>(pTables + m_NumTables); for(Uint32 res=0; res < TotalResources; ++res) new(pCurrResPtr + res) Resource(); for (Uint32 t = 0; t < NumTables; ++t) { new(&GetRootTable(t)) RootTable(TableSizes[t], TableSizes[t] > 0 ? pCurrResPtr : nullptr); pCurrResPtr += TableSizes[t]; } } } |
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:
1 2 3 4 |
RootTable& ShaderResourceCacheD3D12::GetRootTable(Uint32 RootIndex) { return reinterpret_cast<RootTable*>(m_pMemory)[RootIndex]; } |
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:
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
template<D3D12_DESCRIPTOR_HEAP_TYPE HeapType> D3D12_CPU_DESCRIPTOR_HANDLE GetShaderVisibleTableCPUDescriptorHandle(Uint32 RootParamInd, Uint32 OffsetFromTableStart = 0) { auto &RootParam = GetRootTable(RootParamInd); D3D12_CPU_DESCRIPTOR_HANDLE CPUDescriptorHandle = {0}; // Descriptor heap allocation is not assigned for dynamic resources or // in a special case when resource cache is used to store static // variable assignments for a shader if( RootParam.m_TableStartOffset != InvalidDescriptorOffset ) { if( HeapType == D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER ) { CPUDescriptorHandle = m_SamplerHeapSpace.GetCpuHandle(RootParam.m_TableStartOffset + OffsetFromTableStart); } else if( HeapType == D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV ) { CPUDescriptorHandle = m_CbvSrvUavHeapSpace.GetCpuHandle(RootParam.m_TableStartOffset + OffsetFromTableStart); } } return CPUDescriptorHandle; } |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
void RootSignature::InitResourceCache(RenderDeviceD3D12Impl *pDeviceD3D12Impl, ShaderResourceCacheD3D12& ResourceCache)const { // Get root table size for every root index // m_RootParams keeps root tables sorted by the array index, not // the root index // Root views are treated as one-descriptor tables std::vector<Uint32> CacheTableSizes(m_RootParams.GetNumRootTables() + m_RootParams.GetNumRootViews()); for(Uint32 rt = 0; rt < m_RootParams.GetNumRootTables(); ++rt) { auto &RootParam = m_RootParams.GetRootTable(rt); CacheTableSizes[RootParam.GetRootIndex()] = RootParam.GetDescriptorTableSize(); } for(Uint32 rv = 0; rv < m_RootParams.GetNumRootViews(); ++rv) { auto &RootParam = m_RootParams.GetRootView(rv); CacheTableSizes[RootParam.GetRootIndex()] = 1; } // Initialize resource cache to hold root tables ResourceCache.Initialize(CacheMemAllocator, CacheTableSizes.size(), CacheTableSizes.data()); // Allocate space in GPU-visible descriptor heap for static and mutable variables only Uint32 TotalSrvCbvUavDescriptors = m_TotalSrvCbvUavSlots[SHADER_VARIABLE_TYPE_STATIC] + m_TotalSrvCbvUavSlots[SHADER_VARIABLE_TYPE_MUTABLE]; Uint32 TotalSamplerDescriptors = m_TotalSamplerSlots[SHADER_VARIABLE_TYPE_STATIC] + m_TotalSamplerSlots[SHADER_VARIABLE_TYPE_MUTABLE]; DescriptorHeapAllocation CbcSrvUavHeapSpace, SamplerHeapSpace; if(TotalSrvCbvUavDescriptors) CbcSrvUavHeapSpace = pDeviceD3D12Impl-> AllocateGPUDescriptors(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV, TotalSrvCbvUavDescriptors); if(TotalSamplerDescriptors) SamplerHeapSpace = pDeviceD3D12Impl-> AllocateGPUDescriptors(D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER, TotalSamplerDescriptors); // Iterate through all root tables and assign start offsets. The tables are tightly packed, so // start offset of table N+1 is start offset of table N plus the size of table N. Uint32 SrvCbvUavTblStartOffset = 0; Uint32 SamplerTblStartOffset = 0; for(Uint32 rt = 0; rt < m_RootParams.GetNumRootTables(); ++rt) { auto &RootParam = m_RootParams.GetRootTable(rt); const auto& D3D12RootParam = static_cast<const D3D12_ROOT_PARAMETER&>(RootParam); auto &RootTableCache = ResourceCache.GetRootTable(RootParam.GetRootIndex()); auto TableSize = RootParam.GetDescriptorTableSize(); auto HeapType = HeapTypeFromRangeType(D3D12RootParam.DescriptorTable.pDescriptorRanges[0].RangeType); // Space for dynamic variables is allocated at every draw call if( RootParam.GetShaderVariableType() != SHADER_VARIABLE_TYPE_DYNAMIC ) { if( HeapType == D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV ) { RootTableCache.m_TableStartOffset = SrvCbvUavTblStartOffset; SrvCbvUavTblStartOffset += TableSize; } else { RootTableCache.m_TableStartOffset = SamplerTblStartOffset; SamplerTblStartOffset += TableSize; } } } ResourceCache.SetDescriptorHeapSpace(std::move(CbcSrvUavHeapSpace), std::move(SamplerHeapSpace)); } |
The RootSignature::InitResourceCache() takes the following steps:
- Populates
CacheTableSizes array that contains root table size, for every root index
- Recall that RootParamsManager class accesses root tables and root views by their array index, not by the root index
- ShaderResourceCacheD3D12::Initialize() takes array of root table sizes indexed by the root index
- Root views are trated as one-descriptor tables
A sildenafil generico viagra person must always have a good sufficient amount of blood supply to the penile organ can cause listless erection. Everyone is at risk for glaucoma, as they age. check these guys viagra pfizer suisse buy super viagra This medication is sure to meet your expectations and enhance your virility. The pricing factor levitra 20mg canada was based on the economy and attract urgently needed foreign investment into the country.
- Allocates memory for all root tables
- Computes total number of static and mutable descriptors in CBV/SRV/UAV and Sampler tables and allocates space in GPU-visible descriptor heaps
- 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
- 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:
1 2 3 4 5 6 |
cbuffer cbFrameAttribs; // Static (register b0) cbuffer cbModelAttribs; // Mutable (register b1) Texture2D g_HeightMap; // Static (register t0) Buffer g_VertexData; // Mutable (register t1) SamplerState g_HeightMap_sampler; // Static (register s0) |
and a pixel shader with the following resources:
1 2 3 4 5 6 7 8 9 |
cbuffer cbLightAttribs; // Static (register b0) Texture2D g_DiffuseTex; // Mutable (register t0) Texture2D g_ShadowMap; // Static (register t1) SamplerState g_DiffuseTex_sampler; // Mutable - must match texture type (register s0) SamplerComparisonState g_ShadowMap_sampler; // Static sampler (register s1) Texture2D g_DynamicLight; // Dynamic (register t2) SamplerState g_DynamicLight_sampler; // Static sampler (register s2) |
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:
Pixel shader resources are shown in the following figure:
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:
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:
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:
- cbFrameAttribs constant buffer will be placed into a root view at root index 0 (root parameter visibility D3D12_SHADER_VISIBILITY_VERTEX)
- cbModelAttribs constant buffer will be placed into a root view at root index 1 (visibility D3D12_SHADER_VISIBILITY_VERTEX)
-
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
- 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:
- 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)
-
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
-
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
-
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:
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:
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:
1 2 3 |
ShaderResourceCacheD3D12 *pResourceCache = m_ParentResLayout.m_pResourceCache; ShaderResourceCacheD3D12::Resource &DstRes = pResourceCache->GetRootTable(GetRootIndex()).GetResource(OffsetFromTableStart + ArrayIndex); |
If the cache contains allocation in a GPU-visible descriptor heap, we will need CPU descriptor handle in this allocation:
1 2 3 |
D3D12_CPU_DESCRIPTOR_HANDLE ShdrVisibleHeapCPUDescriptorHandle = pResourceCache->GetShaderVisibleTableCPUDescriptorHandle<D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV>( GetRootIndex(), OffsetFromTableStart+ArrayIndex); |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
void ShaderResourceLayoutD3D12::SRV_CBV_UAV::CacheCB(IDeviceObject *pBuffer, ShaderResourceCacheD3D12::Resource& DstRes, Uint32 ArrayInd, D3D12_CPU_DESCRIPTOR_HANDLE ShdrVisibleHeapCPUDescriptorHandle) { RefCntAutoPtr<BufferD3D12Impl> pBuffD3D12(pBuffer, IID_BufferD3D12); DstRes.Type = GetResType(); DstRes.CPUDescriptorHandle = pBuffD3D12->GetCBVHandle(); if(ShdrVisibleHeapCPUDescriptorHandle.ptr != 0 ) { ID3D12Device *pd3d12Device = m_ParentResLayout.m_pd3d12Device; pd3d12Device->CopyDescriptorsSimple(1, ShdrVisibleHeapCPUDescriptorHandle, DstRes.CPUDescriptorHandle, D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV); } DstRes.pObject = pBuffD3D12; } |
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.