最近看了一下《DirectX 12 3D 游戏开发实战》,也就是俗称的龙书。这是一本好书,介绍了DirectX 12 的一些基础知识。
但是当读到第七章时候,发现一个问题,这本书仅仅介绍了一下DirectX 12 API怎么用,但是没有讲一些更深入的东西,这样是无法应用到工程上的。
其中的一个比较困惑的问题就是,既然DirectX 12 希望开发者自己管理描述符,按照龙书那种做法(本程序有多少要渲染的SRV就申请多大的描述符堆)肯定是不行的,那有没有什么推荐的方案呢?
LerningDirectX12
参考Jeremiah van Oosten的GitHub工程,以及其配套的教程,整理了一种可行的管理方案。
总体上思路比较简单。描述符堆分为CPU可见和GPU可见,CPU可见的描述符堆是用来存放所有资源描述符的,GPU可见的描述符用于绑定到根签名。也就是说,需要绑定到根签名描述符表的描述符,会在绘制的时候从CPU可见的描述符堆中拷贝到GPU可见的描述符中。
下面看一下主要代码逻辑。
CPU可见描述符
以贴图的SRV为例,从创建一张贴图来看。
Texture::Texture( Device& device, const D3D12_RESOURCE_DESC& resourceDesc, const D3D12_CLEAR_VALUE* clearValue )
: Resource( device, resourceDesc, clearValue )
{
CreateViews();
}
贴图的构造函数调用了CreateViews。
void Texture::CreateViews()
{
if ( m_d3d12Resource )
{
auto d3d12Device = m_Device.GetD3D12Device();
CD3DX12_RESOURCE_DESC desc( m_d3d12Resource->GetDesc() );
// Create RTV
if ( ( desc.Flags & D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET ) != 0 && CheckRTVSupport() )
{
m_RenderTargetView = m_Device.AllocateDescriptors( D3D12_DESCRIPTOR_HEAP_TYPE_RTV );
d3d12Device->CreateRenderTargetView( m_d3d12Resource.Get(), nullptr,
m_RenderTargetView.GetDescriptorHandle() );
}
// Create DSV
if ( ( desc.Flags & D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL ) != 0 && CheckDSVSupport() )
{
m_DepthStencilView = m_Device.AllocateDescriptors( D3D12_DESCRIPTOR_HEAP_TYPE_DSV );
d3d12Device->CreateDepthStencilView( m_d3d12Resource.Get(), nullptr,
m_DepthStencilView.GetDescriptorHandle() );
}
// Create SRV
if ( ( desc.Flags & D3D12_RESOURCE_FLAG_DENY_SHADER_RESOURCE ) == 0 && CheckSRVSupport() )
{
m_ShaderResourceView = m_Device.AllocateDescriptors( D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV );
d3d12Device->CreateShaderResourceView( m_d3d12Resource.Get(), nullptr,
m_ShaderResourceView.GetDescriptorHandle() );
}
// Create UAV for each mip (only supported for 1D and 2D textures).
if ( ( desc.Flags & D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS ) != 0 && CheckUAVSupport() &&
desc.DepthOrArraySize == 1 )
{
m_UnorderedAccessView =
m_Device.AllocateDescriptors( D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV, desc.MipLevels );
for ( int i = 0; i < desc.MipLevels; ++i )
{
auto uavDesc = GetUAVDesc( desc, i );
d3d12Device->CreateUnorderedAccessView( m_d3d12Resource.Get(), nullptr, &uavDesc,
m_UnorderedAccessView.GetDescriptorHandle( i ) );
}
}
}
}
CreateViews函数判断要创建的贴图是什么类型的贴图,例如要创建一般的贴图,代码就会运行到创建SRV的分支。从代码可以看出创建贴图的同时也会去创建描述符,接下来看一下m_Device.AllocateDescriptors做了什么。
DescriptorAllocation Device::AllocateDescriptors( D3D12_DESCRIPTOR_HEAP_TYPE type, uint32_t numDescriptors )
{
return m_DescriptorAllocators[type]->Allocate( numDescriptors );
}
代码里抽象出一个叫做描述符分配器(DescriptorAllocator)的对象,所有的CPU可见描述符都需要用它来分配生成。
DescriptorAllocation DescriptorAllocator::Allocate( uint32_t numDescriptors )
{
// 多线程保护
std::lock_guard<std::mutex> lock( m_AllocationMutex );
DescriptorAllocation allocation;
// 从所有可用的描述符堆中找到可用的堆
auto iter = m_AvailableHeaps.begin();
while ( iter != m_AvailableHeaps.end() )
{
// 遍历每一个描述符页
auto allocatorPage = m_HeapPool[*iter];
// 通过描述符页分配描述符
allocation = allocatorPage->Allocate( numDescriptors );
if ( allocatorPage->NumFreeHandles() == 0 )
{
iter = m_AvailableHeaps.erase( iter );
}
else
{
++iter;
}
// 如果分配了一个不为空的DescriptorAllocation则完成遍历
if ( !allocation.IsNull() )
{
break;
}
}
// 遍历完所有描述符堆都没有成功创建出DescriptorAllocation则创建一个新的描述符页
if ( allocation.IsNull() )
{
m_NumDescriptorsPerHeap = std::max( m_NumDescriptorsPerHeap, numDescriptors );
auto newPage = CreateAllocatorPage();
allocation = newPage->Allocate( numDescriptors );
}
return allocation;
}
从代码可以看到,描述符分配器包含了多个描述符页(DescriptorAllocatorPage),每个描述符页就是一个描述符堆(ID3D12DescriptorHeap)的包装。如果描述符满了就返回空DescriptorAllocation;如果描述符没满就返回新创建的DescriptorAllocation。如果所有描述符页(也就是描述符堆)都满了没有成功分配DescriptorAllocation,就创建一个新的描述符页来创建DescriptorAllocation。这里说的DescriptorAllocation就是描述符(D3D12_CPU_DESCRIPTOR_HANDLE)的包装。
dx12lib::DescriptorAllocation DescriptorAllocatorPage::Allocate( uint32_t numDescriptors )
{
std::lock_guard<std::mutex> lock( m_AllocationMutex );
if ( numDescriptors > m_NumFreeHandles )
{
return dx12lib::DescriptorAllocation();
}
// ... //
return DescriptorAllocation(
CD3DX12_CPU_DESCRIPTOR_HANDLE( m_BaseDescriptor, offset, m_DescriptorHandleIncrementSize ), numDescriptors,
m_DescriptorHandleIncrementSize, shared_from_this() );
}
从代码大致可以看出来,如果还有空间申请描述符,就创建一个描述符并返回,如果没有空间了就返回一个空的描述符。
回到代码最初的位置,根据以上逻辑就获得了一张贴图的SRV并存放在合适的描述符堆上。
当然还有一个函数被我们忽略了,那就是申请CPU可见描述符堆是怎么申请的?按照以上过程,申请新的描述符堆也就是申请新的描述符页。下面是描述符页的构造函数。
DescriptorAllocatorPage::DescriptorAllocatorPage( Device& device, D3D12_DESCRIPTOR_HEAP_TYPE type,
uint32_t numDescriptors )
: m_Device( device )
, m_HeapType( type )
, m_NumDescriptorsInHeap( numDescriptors )
{
auto d3d12Device = m_Device.GetD3D12Device();
D3D12_DESCRIPTOR_HEAP_DESC heapDesc = {};
heapDesc.Type = m_HeapType;
heapDesc.NumDescriptors = m_NumDescriptorsInHeap;
ThrowIfFailed( d3d12Device->CreateDescriptorHeap( &heapDesc, IID_PPV_ARGS( &m_d3d12DescriptorHeap ) ) );
m_BaseDescriptor = m_d3d12DescriptorHeap->GetCPUDescriptorHandleForHeapStart();
m_DescriptorHandleIncrementSize = d3d12Device->GetDescriptorHandleIncrementSize( m_HeapType );
m_NumFreeHandles = m_NumDescriptorsInHeap;
// Initialize the free lists
AddNewBlock( 0, m_NumFreeHandles );
}
从heapDesc对象的成员的赋值来看,并没有为它的Flags字段赋值,也即是说Flags字段默认为D3D12_DESCRIPTOR_HEAP_FLAG_NONE,即CPU可见。
GPU可见描述符
在主循环渲染函数里,我们需要将图片的SRV绑定到根签名上,以下例程展示了渲染主循环里的一段代码。
void TutorialX::OnRender()
{
auto& commandQueue = m_Device->GetCommandQueue( D3D12_COMMAND_LIST_TYPE_DIRECT );
auto commandList = commandQueue.GetCommandList();
// Clear the render targets.
{
FLOAT clearColor[] = { 0.4f, 0.6f, 0.9f, 1.0f };
commandList->ClearTexture( m_RenderTarget.GetTexture( AttachmentPoint::Color0 ), clearColor );
commandList->ClearDepthStencilTexture( m_RenderTarget.GetTexture( AttachmentPoint::DepthStencil ),
D3D12_CLEAR_FLAG_DEPTH );
}
commandList->SetPipelineState( m_PipelineState );
commandList->SetGraphicsRootSignature( m_RootSignature );
// ... //
// 绑定图片到根签名的指定根参数索引位置
commandList->SetShaderResourceView( RootParameters::Textures, 0, m_EarthTexture,
D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE );
// 绘制图形
commandList.Draw( vertexCount, instanceCount, 0u, startInstance );
}
以上代码里,我们最关心的两个函数调用是SetShaderResourceView和Draw。
SetShaderResourceView
void CommandList::SetShaderResourceView( int32_t rootParameterIndex, uint32_t descriptorOffset,
const std::shared_ptr<Texture>& texture, D3D12_RESOURCE_STATES stateAfter,
UINT firstSubresource, UINT numSubresources )
{
if ( texture )
{
// ... //
m_DynamicDescriptorHeap[D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV]->StageDescriptors(
rootParameterIndex, descriptorOffset, 1, texture->GetShaderResourceView() );
}
}
以上代码只保留了最关键部分。可见在CommandList里有一个动态描述符堆(DynamicDescriptorHeap)的对象,这个动态描述符堆就是用来绑定到根签名上的。这个绑定过程,通过StageDescriptors这个函数了实现,这个函数的含义是“暂存描述符”。
void DynamicDescriptorHeap::StageDescriptors( uint32_t rootParameterIndex, uint32_t offset, uint32_t numDescriptors,
const D3D12_CPU_DESCRIPTOR_HANDLE srcDescriptor )
{
// 不能暂存多于单个堆的最大描述符数。
// 不能暂存多于根参数最大个数MaxDescriptorTables。
if ( numDescriptors > m_NumDescriptorsPerHeap || rootParameterIndex >= MaxDescriptorTables )
{
throw std::bad_alloc();
}
// 根据根参数索引,获取根描述符表的缓存
DescriptorTableCache& descriptorTableCache = m_DescriptorTableCache[rootParameterIndex];
// 检查要拷贝的描述符数量不超过描述符表期望的描述符数。
if ( ( offset + numDescriptors ) > descriptorTableCache.NumDescriptors )
{
throw std::length_error( "Number of descriptors exceeds the number of descriptors in the descriptor table." );
}
// 暂存到对应缓存里
D3D12_CPU_DESCRIPTOR_HANDLE* dstDescriptor = ( descriptorTableCache.BaseDescriptor + offset );
for ( uint32_t i = 0; i < numDescriptors; ++i )
{ dstDescriptor[i] = CD3DX12_CPU_DESCRIPTOR_HANDLE( srcDescriptor, i, m_DescriptorHandleIncrementSize ); }
// 设置根参数索引到一个整数位里,确保描述符表里在指定索引地方的描述符被绑定到命令列表里。
m_StaleDescriptorTableBitMask |= ( 1 << rootParameterIndex );
}
从以上代码可以看出,暂存描述符就是把描述符的CPU指针赋值到对应缓存上。函数最后有一个位操作,用一个32位数字存放了根参数的32个索引是否已经暂存了描述符,也就是说根签名最多支持32个根参数。
Draw
void CommandList::Draw( uint32_t vertexCount, uint32_t instanceCount, uint32_t startVertex, uint32_t startInstance )
{
FlushResourceBarriers();
for ( int i = 0; i < D3D12_DESCRIPTOR_HEAP_TYPE_NUM_TYPES; ++i )
{
m_DynamicDescriptorHeap[i]->CommitStagedDescriptorsForDraw( *this );
}
m_d3d12CommandList->DrawInstanced( vertexCount, instanceCount, startVertex, startInstance );
}
从Draw函数看,主要做了两件事,第一是将描述符提交到管线,第二是调用绘制命令。我们目前关心的是如何将描述符提交到管线。
void DynamicDescriptorHeap::CommitStagedDescriptorsForDraw( CommandList& commandList )
{
CommitDescriptorTables( commandList, &ID3D12GraphicsCommandList::SetGraphicsRootDescriptorTable );
CommitInlineDescriptors( commandList, m_InlineCBV, m_StaleCBVBitMask,
&ID3D12GraphicsCommandList::SetGraphicsRootConstantBufferView );
CommitInlineDescriptors( commandList, m_InlineSRV, m_StaleSRVBitMask,
&ID3D12GraphicsCommandList::SetGraphicsRootShaderResourceView );
CommitInlineDescriptors( commandList, m_InlineUAV, m_StaleUAVBitMask,
&ID3D12GraphicsCommandList::SetGraphicsRootUnorderedAccessView );
}
内联描述符比较简单,我们关心描述符表,所以继续深入看CommitDescriptorTables。
void DynamicDescriptorHeap::CommitDescriptorTables(
CommandList& commandList,
std::function<void( ID3D12GraphicsCommandList*, UINT, D3D12_GPU_DESCRIPTOR_HANDLE )> setFunc )
{
// 计算需要复制的描述符数量。
uint32_t numDescriptorsToCommit = ComputeStaleDescriptorCount();
if ( numDescriptorsToCommit > 0 )
{
auto d3d12Device = m_Device.GetD3D12Device();
auto d3d12GraphicsCommandList = commandList.GetD3D12CommandList().Get();
assert( d3d12GraphicsCommandList != nullptr );
if ( !m_CurrentDescriptorHeap || m_NumFreeHandles < numDescriptorsToCommit )
{
// 创建GPU可见描述符堆。
m_CurrentDescriptorHeap = RequestDescriptorHeap();
m_CurrentCPUDescriptorHandle = m_CurrentDescriptorHeap->GetCPUDescriptorHandleForHeapStart();
m_CurrentGPUDescriptorHandle = m_CurrentDescriptorHeap->GetGPUDescriptorHandleForHeapStart();
m_NumFreeHandles = m_NumDescriptorsPerHeap;
// 将GPU可见描述符堆设置到命令列表里,生成GPU地址。
commandList.SetDescriptorHeap( m_DescriptorHeapType, m_CurrentDescriptorHeap.Get() );
// When updating the descriptor heap on the command list, all descriptor
// tables must be (re)recopied to the new descriptor heap (not just
// the stale descriptor tables).
m_StaleDescriptorTableBitMask = m_DescriptorTableBitMask;
}
DWORD rootIndex;
// 遍历根签名的所有根参数。
while ( _BitScanForward( &rootIndex, m_StaleDescriptorTableBitMask ) )
{
UINT numSrcDescriptors = m_DescriptorTableCache[rootIndex].NumDescriptors;
D3D12_CPU_DESCRIPTOR_HANDLE* pSrcDescriptorHandles = m_DescriptorTableCache[rootIndex].BaseDescriptor;
D3D12_CPU_DESCRIPTOR_HANDLE pDestDescriptorRangeStarts[] = { m_CurrentCPUDescriptorHandle };
UINT pDestDescriptorRangeSizes[] = { numSrcDescriptors };
// 将所有CPU可见描述符拷贝到GPU可见描述符堆上。
d3d12Device->CopyDescriptors( 1, pDestDescriptorRangeStarts, pDestDescriptorRangeSizes, numSrcDescriptors,
pSrcDescriptorHandles, nullptr, m_DescriptorHeapType );
// 通过传进函数的setter函数,将描述符设置到命令列表上。
setFunc( d3d12GraphicsCommandList, rootIndex, m_CurrentGPUDescriptorHandle );
// 偏移当前CPU和GPU描述符句柄。
m_CurrentCPUDescriptorHandle.Offset( numSrcDescriptors, m_DescriptorHandleIncrementSize );
m_CurrentGPUDescriptorHandle.Offset( numSrcDescriptors, m_DescriptorHandleIncrementSize );
m_NumFreeHandles -= numSrcDescriptors;
// 反转已经拷贝过的位,防止再次拷贝。
m_StaleDescriptorTableBitMask ^= ( 1 << rootIndex );
}
}
}
从上面代码可以看出来,CommitDescriptorTables才是真正将描述传到GPU的函数。函数首先创建了GPU可见的描述符堆,然后遍历所有暂存的描述符,最后将暂存描述符,也就是CPU可见描述符,拷贝到GPU可见描述符的地址上。
当然还有一个函数被我们忽略了,那就是申请GPU可见描述符堆是怎么申请的?深入上面代码的RequestDescriptorHeap函数,里调用了一个叫做CreateDescriptorHeap的函数,深入CreateDescriptorHeap看看是怎么实现的。
Microsoft::WRL::ComPtr<ID3D12DescriptorHeap> DynamicDescriptorHeap::CreateDescriptorHeap()
{
auto d3d12Device = m_Device.GetD3D12Device();
D3D12_DESCRIPTOR_HEAP_DESC descriptorHeapDesc = {};
descriptorHeapDesc.Type = m_DescriptorHeapType;
descriptorHeapDesc.NumDescriptors = m_NumDescriptorsPerHeap;
descriptorHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
Microsoft::WRL::ComPtr<ID3D12DescriptorHeap> descriptorHeap;
ThrowIfFailed( d3d12Device->CreateDescriptorHeap( &descriptorHeapDesc, IID_PPV_ARGS( &descriptorHeap ) ) );
return descriptorHeap;
}
从descriptorHeapDesc对象的成员的赋值来看,为它的Flags字段赋值了D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE,即GPU可见。
小结
这就是完整的描述符管理逻辑。总结一下,CPU可见描述符存储所有已经产生资源的、需要管理的描述符,本方案用一种分页和类似数组的方式管理CPU可见描述符堆,对于描述符的分配和释放都比较灵活;GPU可见描述符是在主循环绘制时候选择需要提交的CPU可见描述符,拷贝到GPU可见描述符堆,再进行提交。
MiniEngine
MiniEngine是微软的使用DirectX 12 实现的一套示例代码,这套代码使用DirectX 12 实现了一个迷你渲染引擎,可以说这套代码就是官方推荐的普适解决方案的集合。
这里是MiniEngine的工程链接。
从工程源码上看,其描述符的管理方式和Jeremiah van Oosten的实现大同小异,或者说Jeremiah van Oosten的实现或许参考了微软官方推荐的方案。
Unreal Engine 4.26
UE4源码关于DirectX 12 的调用都封装到D3D12RHI模块里了。
Offline Descriptor
同样我们还是从贴图入手,打开“UnrealEngine-4.26/Engine/Source/Runtime/D3D12RHI/Private/D3D12Texture.cpp”,找到CreateD3D12Texture2D函数。以下代码为删除我们不关心过程后的代码。
template<typename BaseResourceType>
TD3D12Texture2D<BaseResourceType>* FD3D12DynamicRHI::CreateD3D12Texture2D(FRHICommandListImmediate* RHICmdList, uint32 SizeX, uint32 SizeY, uint32 SizeZ, bool bTextureArray, bool bCubeTexture, EPixelFormat Format,
uint32 NumMips, uint32 NumSamples, ETextureCreateFlags Flags, FRHIResourceCreateInfo& CreateInfo)
{
// ... //
TD3D12Texture2D<BaseResourceType>* NewTexture = new TD3D12Texture2D<BaseResourceType>(Device,
SizeX,
SizeY,
SizeZ,
NumMips,
ActualMSAACount,
(EPixelFormat)Format,
bCubeTexture,
Flags,
CreateInfo.ClearValueBinding);
// ... //
if (bCreateShaderResource)
{
// ... //
NewTexture->SetShaderResourceView(new FD3D12ShaderResourceView(Device, SRVDesc, Location));
}
// ... //
}
从代码可以看出,新创建的Resource为NewTexture,对应的为NewTexture创建的SRV就是通过FD3D12ShaderResourceView的构造函数来创建的。创建出来SRV之后通过SetShaderResourceView赋值给贴图,这个函数就是个简单的赋值语句,也就不详细看了。
FD3D12ShaderResourceView类本身包含一个描述符成员变量Descriptor,下面进入FD3D12ShaderResourceView构造函数看看。
FD3D12ShaderResourceView(FD3D12Device* InParent, D3D12_SHADER_RESOURCE_VIEW_DESC& InDesc, FD3D12ResourceLocation& InResourceLocation, uint32 InStride = -1, bool InSkipFastClearFinalize = false)
: FD3D12ShaderResourceView(InParent)
{
Initialize(InDesc, InResourceLocation, InStride, InSkipFastClearFinalize);
}
进入Initialize函数看一下。
void Initialize(D3D12_SHADER_RESOURCE_VIEW_DESC& InDesc, FD3D12ResourceLocation& InResourceLocation, uint32 InStride, bool InSkipFastClearFinalize = false)
{
// ... //
CreateView(InDesc, InResourceLocation);
}
进入CreateView看一下。
void CreateView(const TDesc& InDesc, FD3D12ResourceLocation& InResourceLocation)
{
Initialize(InDesc, InResourceLocation);
ID3D12Resource* D3DResource = ResourceLocation->GetResource()->GetResource();
Descriptor.CreateView(Desc, D3DResource);
}
从这里可以看出,这个函数就是在创建SRV的描述符了。那么代码里面的这个Descriptor是从哪里来的呢?Descriptor里面有一个成员Handle,也就是CPU可见的描述符句柄,下面看看这个句柄是如何创建出来的。
template <typename TDesc>
void TD3D12ViewDescriptorHandle<TDesc>::AllocateDescriptorSlot()
{
if (Parent)
{
FD3D12Device* Device = GetParentDevice();
FD3D12OfflineDescriptorManager& DescriptorAllocator = Device->template GetViewDescriptorAllocator<TDesc>();
Handle = DescriptorAllocator.AllocateHeapSlot(Index);
check(Handle.ptr != 0);
}
}
又来到我们熟悉的描述符分配器DescriptorAllocator的逻辑了。进入AllocateHeapSlot看一下。
HeapOffset AllocateHeapSlot(HeapIndex &outIndex)
{
FScopeLock Lock(&CritSect);
if (0 == m_FreeHeaps.Num())
{
AllocateHeap();
}
// ... //
}
进入FD3D12OfflineDescriptorManager类的AllocateHeap函数里面看一下。
void AllocateHeap()
{
TRefCountPtr<ID3D12DescriptorHeap> Heap;
VERIFYD3D12RESULT(m_pDevice->CreateDescriptorHeap(&m_Desc, IID_PPV_ARGS(Heap.GetInitReference())));
SetName(Heap, L"FD3D12OfflineDescriptorManager Descriptor Heap");
HeapOffset HeapBase = Heap->GetCPUDescriptorHandleForHeapStart();
check(HeapBase.ptr != 0);
// Allocate and initialize a single new entry in the map
// ... //
}
又看到我们熟悉的CreateDescriptorHeap函数了,这里我们甚至可以猜测m_Desc的Flags设置的是什么,没错就是CPU可见。
static D3D12_DESCRIPTOR_HEAP_DESC CreateDescriptor(FRHIGPUMask Node, D3D12_DESCRIPTOR_HEAP_TYPE Type, uint32 NumDescriptorsPerHeap)
{
D3D12_DESCRIPTOR_HEAP_DESC Desc = {};
Desc.Type = Type;
Desc.NumDescriptors = NumDescriptorsPerHeap;
Desc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;// None as this heap is offline
Desc.NodeMask = Node.GetNative();
return Desc;
}
在UE4里,CPU可见描述符它给换了个叫法,叫做离线描述符(Offline Descriptor),AllocateHeap函数所属的类才叫做FD3D12OfflineDescriptorManager。
Online Descriptor
接下来按照之前的阅读顺序,我们应该去关心一下GPU可见描述符是如何管理的了。打开“UnrealEngine-4.26/Engine/Source/untime/D3D12RHI/Private/D3D12Commands.cpp”找到FD3D12CommandContext::RHIDrawPrimitive函数,从这里开始看。
void FD3D12CommandContext::RHIDrawPrimitive(uint32 BaseVertexIndex, uint32 NumPrimitives, uint32 NumInstances)
{
RHI_DRAW_CALL_STATS(StateCache.GetGraphicsPipelinePrimitiveType(), FMath::Max(NumInstances, 1U) * NumPrimitives);
CommitGraphicsResourceTables();
CommitNonComputeShaderConstants();
// ... //
StateCache.ApplyState<D3D12PT_Graphics>();
CommandListHandle->DrawInstanced(VertexCount, NumInstances, BaseVertexIndex, 0);
// ... //
}
这个函数里我们最关心的两个调用就是CommitGraphicsResourceTables和StateCache.ApplyState
首先来看看CommitGraphicsResourceTables。
void FD3D12CommandContext::CommitGraphicsResourceTables()
{
//SCOPE_CYCLE_COUNTER(STAT_D3D12CommitResourceTables);
const FD3D12GraphicsPipelineState* const RESTRICT GraphicPSO = StateCache.GetGraphicsPipelineState();
check(GraphicPSO);
auto* PixelShader = GraphicPSO->GetPixelShader();
if (PixelShader)
{
SetUAVPSResourcesFromTables(PixelShader);
}
if (auto* Shader = GraphicPSO->GetVertexShader())
{
SetResourcesFromTables(Shader);
}
if (PixelShader)
{
SetResourcesFromTables(PixelShader);
}
if (auto* Shader = GraphicPSO->GetHullShader())
{
SetResourcesFromTables(Shader);
}
if (auto* Shader = GraphicPSO->GetDomainShader())
{
SetResourcesFromTables(Shader);
}
if (auto* Shader = GraphicPSO->GetGeometryShader())
{
SetResourcesFromTables(Shader);
}
}
进入SetResourcesFromTables看一下。
template <class ShaderType>
void FD3D12CommandContext::SetResourcesFromTables(const ShaderType* RESTRICT Shader)
{
checkSlow(Shader);
// Mask the dirty bits by those buffers from which the shader has bound resources.
uint32 DirtyBits = Shader->ShaderResourceTable.ResourceTableBits & DirtyUniformBuffers[ShaderType::StaticFrequency];
while (DirtyBits)
{
// Scan for the lowest set bit, compute its index, clear it in the set of dirty bits.
const uint32 LowestBitMask = (DirtyBits)& (-(int32)DirtyBits);
const int32 BufferIndex = FMath::FloorLog2(LowestBitMask); // todo: This has a branch on zero, we know it could never be zero...
DirtyBits ^= LowestBitMask;
FD3D12UniformBuffer* Buffer = BoundUniformBuffers[ShaderType::StaticFrequency][BufferIndex];
check(Buffer);
check(BufferIndex < Shader->ShaderResourceTable.ResourceTableLayoutHashes.Num());
check(Buffer->GetLayout().GetHash() == Shader->ShaderResourceTable.ResourceTableLayoutHashes[BufferIndex]);
// todo: could make this two pass: gather then set
SetShaderResourcesFromBuffer_Surface<(EShaderFrequency)ShaderType::StaticFrequency>(*this, Buffer, Shader->ShaderResourceTable.TextureMap.GetData(), BufferIndex);
SetShaderResourcesFromBuffer_SRV<(EShaderFrequency)ShaderType::StaticFrequency>(*this, Buffer, Shader->ShaderResourceTable.ShaderResourceViewMap.GetData(), BufferIndex);
SetShaderResourcesFromBuffer_Sampler<(EShaderFrequency)ShaderType::StaticFrequency>(*this, Buffer, Shader->ShaderResourceTable.SamplerMap.GetData(), BufferIndex);
}
DirtyUniformBuffers[ShaderType::StaticFrequency] = 0;
}
这里我们比较关心SetShaderResourcesFromBuffer_SRV函数的调用,可以看到这里会把SRV传进去处理,这个函数里还会调用一个叫做SetResource的函数,我们直接进函数里去看。
template <EShaderFrequency Frequency>
FORCEINLINE void SetResource(FD3D12CommandContext& CmdContext, uint32 BindIndex, FD3D12ShaderResourceView* RESTRICT SRV)
{
// We set the resource through the RHI to track state for the purposes of unbinding SRVs when a UAV or RTV is bound.
CmdContext.StateCache.SetShaderResourceView<Frequency>(SRV, BindIndex);
}
再深入SetShaderResourceView看看。
template <EShaderFrequency ShaderFrequency>
void FD3D12StateCacheBase::SetShaderResourceView(FD3D12ShaderResourceView* SRV, uint32 ResourceIndex)
{
// ... //
FD3D12ShaderResourceViewCache& Cache = PipelineState.Common.SRVCache;
auto& CurrentShaderResourceViews = Cache.Views[ShaderFrequency];
// ... //
CurrentShaderResourceViews[ResourceIndex] = SRV;
FD3D12ShaderResourceViewCache::DirtySlot(Cache.DirtySlotMask[ShaderFrequency], ResourceIndex);
// ... //
}
这里我们主要关心SRV被赋值给了一个FD3D12ShaderResourceViewCache,并且设置了Slot为脏数据。
下面我们回到最初的RHIDrawPrimitive函数里看一下,进入我们关心的第二个函数StateCache.ApplyState
template <ED3D12PipelineType PipelineType>
void FD3D12StateCacheBase::ApplyState()
{
// ... //
FD3D12ShaderResourceViewCache& SRVCache = PipelineState.Common.SRVCache;
#define CONDITIONAL_SET_SRVS(Shader) \
if (CurrentShaderDirtySRVSlots[##Shader]) \
{ \
DescriptorCache.SetSRVs<##Shader>(pRootSignature, SRVCache, CurrentShaderDirtySRVSlots[##Shader], NumSRVs[##Shader], ViewHeapSlot); \
}
// ... //
}
从代码可以看出刚才缓存的SRV被取出来,传入DescriptorCache.SetSRVs函数处理。
template <EShaderFrequency ShaderStage>
void FD3D12DescriptorCache::SetSRVs(const FD3D12RootSignature* RootSignature, FD3D12ShaderResourceViewCache& Cache, const SRVSlotMask& SlotsNeededMask, uint32 SlotsNeeded, uint32& HeapSlot)
{
// ... //
FD3D12CommandListHandle& CommandList = CmdContext->CommandListHandle;
auto& SRVs = Cache.Views[ShaderStage];
// Reserve heap slots
uint32 FirstSlotIndex = HeapSlot;
HeapSlot += SlotsNeeded;
D3D12_CPU_DESCRIPTOR_HANDLE DestDescriptor = CurrentViewHeap->GetCPUSlotHandle(FirstSlotIndex);
D3D12_CPU_DESCRIPTOR_HANDLE SrcDescriptors[MAX_SRVS];
for (uint32 SlotIndex = 0; SlotIndex < SlotsNeeded; SlotIndex++)
{
if (SRVs[SlotIndex] != nullptr)
{
SrcDescriptors[SlotIndex] = SRVs[SlotIndex]->GetView();
// ... //
}
else
{
SrcDescriptors[SlotIndex] = pNullSRV->GetHandle();
}
check(SrcDescriptors[SlotIndex].ptr != 0);
}
FD3D12ShaderResourceViewCache::CleanSlots(CurrentDirtySlotMask, SlotsNeeded);
ID3D12Device* Device = GetParentDevice()->GetDevice();
Device->CopyDescriptors(1, &DestDescriptor, &SlotsNeeded, SlotsNeeded, SrcDescriptors, nullptr, D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
const D3D12_GPU_DESCRIPTOR_HANDLE BindDescriptor = CurrentViewHeap->GetGPUSlotHandle(FirstSlotIndex);
if (ShaderStage == SF_Compute)
{
const uint32 RDTIndex = RootSignature->SRVRDTBindSlot(ShaderStage);
CommandList->SetComputeRootDescriptorTable(RDTIndex, BindDescriptor);
}
else
{
const uint32 RDTIndex = RootSignature->SRVRDTBindSlot(ShaderStage);
CommandList->SetGraphicsRootDescriptorTable(RDTIndex, BindDescriptor);
}
}
从代码可以看到函数里面,把缓存的SRV拷贝到了CurrentViewHeap里面了,最终通过SetGraphicsRootDescriptorTable设置给了跟描述符表。
这里CurrentViewHeap的类型为FD3D12OnlineHeap,不出所料GPU可见的描述符在UE4里叫做在线描述符(Online Descriptor),同时如我们所料在它的初始化函数里,就是创建了一个GPU可见的描述符堆。
void FD3D12LocalOnlineHeap::Init(uint32 NumDescriptors, D3D12_DESCRIPTOR_HEAP_TYPE Type)
{
Desc = {};
Desc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
Desc.Type = Type;
Desc.NumDescriptors = NumDescriptors;
Desc.NodeMask = GetGPUMask().GetNative();
//LLM_SCOPE(ELLMTag::DescriptorCache);
VERIFYD3D12RESULT(GetParentDevice()->GetDevice()->CreateDescriptorHeap(&Desc, IID_PPV_ARGS(Heap.GetInitReference())));
SetName(Heap, Desc.Type == D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV ? L"Thread Local - Online View Heap" : L"Thread Local - Online Sampler Heap");
Entry.Heap = Heap;
CPUBase = Heap->GetCPUDescriptorHandleForHeapStart();
GPUBase = Heap->GetGPUDescriptorHandleForHeapStart();
DescriptorSize = GetParentDevice()->GetDevice()->GetDescriptorHandleIncrementSize(Type);
// ... //
}
这里还有点小问题,就是既然创建了GPU可见描述符,那么是哪里设置给命令列表的呢?这里搜索FD3D12DescriptorCache::SetDescriptorHeaps,看一下调用,发现每次绘制开始都会去调用。进入函数去看。
bool FD3D12DescriptorCache::SetDescriptorHeaps()
{
// ... //
if (bHeapChanged)
{
ID3D12DescriptorHeap* /*const*/ ppHeaps[] = { pCurrentViewHeap, pCurrentSamplerHeap };
CmdContext->CommandListHandle->SetDescriptorHeaps(UE_ARRAY_COUNT(ppHeaps), ppHeaps);
pPreviousViewHeap = pCurrentViewHeap;
pPreviousSamplerHeap = pCurrentSamplerHeap;
}
// ... //
}
可见UE4是发现有堆改变才会去调用命令列表的SetDescriptorHeaps函数。
总结
至此,描述符的管理在三套代码里都给出了相同的方案,这样就解决了龙书里只能固定分配好资源和描述符,对于需要动态分配的资源束手无策的问题。龙书的例程作为讲解API的Demo,不可能考虑很周全,学习龙书更要学习引擎代码,了解一个知识是如何被应用到工程里面的。