学习CommandAllocatorPool类
- 专栏前言
- CommandAllocatorPool
-
- [(1) 源码展示](#(1) 源码展示)
- [(2) 源码分析](#(2) 源码分析)
- [(3) 类使用分析](#(3) 类使用分析)
- [(4) 类总结](#(4) 类总结)
- [(5) 最终代码](#(5) 最终代码)
专栏前言
- 最近想要开发一个游戏引擎,使用D3D12作为图形API,于是在看D3D12-MiniEngine。
- MiniEngine是微软DirectX示例库中的一个项目,链接为DirectX-Graphics-Samples。
- 我的初步想法是基于MiniEngine集成一套RenderContext,即渲染上下文,支持完整的渲染工作流。然后在此之上在集成一套RenderGraph,即渲染依赖图,支持各种渲染后处理和优化。
- 本专栏文章主要分析MiniEngine库代码,逐步将其拆解为独立的、完整的组件,为自定义项目开发提供技术支撑。
CommandAllocatorPool
(1) 源码展示
- 类头文件如下:
cpp
#pragma once
#include <vector>
#include <queue>
#include <mutex>
#include <stdint.h>
class CommandAllocatorPool
{
public:
CommandAllocatorPool(D3D12_COMMAND_LIST_TYPE Type);
~CommandAllocatorPool();
void Create(ID3D12Device* pDevice);
void Shutdown();
ID3D12CommandAllocator* RequestAllocator(uint64_t CompletedFenceValue);
void DiscardAllocator(uint64_t FenceValue, ID3D12CommandAllocator* Allocator);
inline size_t Size() { return mAllocatorPool.size(); }
private:
const D3D12_COMMAND_LIST_TYPE mCommandListType;
ID3D12Device* mDevice;
std::vector<ID3D12CommandAllocator*> mAllocatorPool;
std::queue<std::pair<uint64_t, ID3D12CommandAllocator*>> mReadyAllocators;
std::mutex mAllocatorMutex;
};
- 类源文件如下:
cpp
#include "pch.h"
#include "CommandAllocatorPool.h"
CommandAllocatorPool::CommandAllocatorPool(D3D12_COMMAND_LIST_TYPE Type) :
mCommandListType(Type),
mDevice(nullptr)
{
}
CommandAllocatorPool::~CommandAllocatorPool()
{
Shutdown();
}
void CommandAllocatorPool::Create(ID3D12Device* pDevice)
{
mDevice = pDevice;
}
void CommandAllocatorPool::Shutdown()
{
for (size_t i = 0; i < mAllocatorPool.size(); ++i)
mAllocatorPool[i]->Release();
mAllocatorPool.clear();
}
ID3D12CommandAllocator* CommandAllocatorPool::RequestAllocator(uint64_t CompletedFenceValue)
{
std::lock_guard<std::mutex> LockGuard(mAllocatorMutex);
ID3D12CommandAllocator* pAllocator = nullptr;
if (!mReadyAllocators.empty())
{
std::pair<uint64_t, ID3D12CommandAllocator*>& AllocatorPair = mReadyAllocators.front();
if (AllocatorPair.first <= CompletedFenceValue)
{
pAllocator = AllocatorPair.second;
ASSERT_SUCCEEDED(pAllocator->Reset());
mReadyAllocators.pop();
}
}
if (pAllocator == nullptr)
{
ASSERT_SUCCEEDED(mDevice->CreateCommandAllocator(mCommandListType, MY_IID_PPV_ARGS(&pAllocator)));
wchar_t AllocatorName[32];
swprintf(AllocatorName, 32, L"CommandAllocator %zu", mAllocatorPool.size());
pAllocator->SetName(AllocatorName);
mAllocatorPool.push_back(pAllocator);
}
return pAllocator;
}
void CommandAllocatorPool::DiscardAllocator(uint64_t FenceValue, ID3D12CommandAllocator* Allocator)
{
std::lock_guard<std::mutex> LockGuard(mAllocatorMutex);
mReadyAllocators.push(std::make_pair(FenceValue, Allocator));
}
(2) 源码分析
类成员变量如下:
cpp
// 命令类型(不可修改)
const D3D12_COMMAND_LIST_TYPE m_cCommandListType;
// D3D12设备对象
ID3D12Device* mDevice;
// 池内所有命令分配器
std::vector<ID3D12CommandAllocator*> mAllocatorPool;
// 池内就绪命令分配器
std::queue<std::pair<uint64_t, ID3D12CommandAllocator*>> mReadyAllocators;
// 类线程锁
std::mutex mAllocatorMutex;
类方法如下:
cpp
// 构造函数,记录命令列表类型,初始化设备指针为空
CommandAllocatorPool::CommandAllocatorPool(D3D12_COMMAND_LIST_TYPE Type) :
m_cCommandListType(Type),
mDevice(nullptr)
{
}
// 析构时调用Shutdown
CommandAllocatorPool::~CommandAllocatorPool()
{
Shutdown();
}
// 创建时保存D3D12设备指针
void CommandAllocatorPool::Create(ID3D12Device* pDevice)
{
mDevice = pDevice;
}
// 关闭(调用前必须保证所有分配器中的命令已被GPU执行)
void CommandAllocatorPool::Shutdown()
{
// 逐个释放所有命令分配器
for (size_t i = 0; i < mAllocatorPool.size(); ++i)
mAllocatorPool[i]->Release();
mAllocatorPool.clear();
}
// 请求一个命令分配器,参数CompletedFenceValue为当前已完成围栏值
ID3D12CommandAllocator* CommandAllocatorPool::RequestAllocator(uint64_t CompletedFenceValue)
{
// 加锁
std::lock_guard<std::mutex> LockGuard(mAllocatorMutex);
ID3D12CommandAllocator* pAllocator = nullptr;
// 查找就绪分配器队列mReadyAllocators
if (!mReadyAllocators.empty())
{
// 获取就绪队列首个分配器,以及表示其是否使用完成的围栏值
std::pair<uint64_t, ID3D12CommandAllocator*>& AllocatorPair = mReadyAllocators.front();
// 若当前完成围栏值 >= 分配器围栏值,则说明此分配器使用完毕,可复用
if (AllocatorPair.first <= CompletedFenceValue)
{
// 调用Reset重置分配器,将其弹出就绪队列,返回给调用者
pAllocator = AllocatorPair.second;
ASSERT_SUCCEEDED(pAllocator->Reset());
mReadyAllocators.pop();
}
}
// 如果没有可复用分配器,则创建分配器并返回
if (pAllocator == nullptr)
{
ASSERT_SUCCEEDED(mDevice->CreateCommandAllocator(m_cCommandListType, MY_IID_PPV_ARGS(&pAllocator)));
// 创建新的分配器并压入pAllocator,pAllocator是单调递增的
// 使用"CommandAllocator + mAllocatorPool.size()"设置新增分配器的名称
wchar_t AllocatorName[32];
swprintf(AllocatorName, 32, L"CommandAllocator %zu", mAllocatorPool.size());
pAllocator->SetName(AllocatorName);
mAllocatorPool.push_back(pAllocator);
}
return pAllocator;
}
// 释放命令分配器,其中FenceValue代表该命令可复用的围栏值
void CommandAllocatorPool::DiscardAllocator(uint64_t FenceValue, ID3D12CommandAllocator* Allocator)
{
// 加锁,并将<FenceValue, Allocator>压入mReadyAllocators
std::lock_guard<std::mutex> LockGuard(mAllocatorMutex);
mReadyAllocators.push(std::make_pair(FenceValue, Allocator));
}
// 返回池内分配器总数
inline size_t Size() { return mAllocatorPool.size(); }
(3) 类使用分析
- 可以看到CommandAllocatorPool封装了固定命令类型的分配器池,用户通过下面接口获取分配器。其中CompletedFenceValue表示当前完成的围栏值。
cpp
ID3D12CommandAllocator* RequestAllocator(uint64_t CompletedFenceValue);
- 用户获取并使用分配器后,通过下面接口归还。mReadyAllocators中记录了归还的分配器,还记录了表示分配器可复用的对应围栏值。
cpp
// 释放命令分配器,其中FenceValue代表该命令可复用的围栏值
void CommandAllocatorPool::DiscardAllocator(uint64_t FenceValue, ID3D12CommandAllocator* Allocator)
{
// 加锁,并将<FenceValue, Allocator>压入mReadyAllocators
std::lock_guard<std::mutex> LockGuard(mAllocatorMutex);
// That fence value indicates we are free to reset the allocator
mReadyAllocators.push(std::make_pair(FenceValue, Allocator));
}
- 然后再回到获取分配器,其逻辑如下。可以看到若有已完成的就绪分配器,则弹出mReadyAllocators并返回给用户。若没有可复用分配器,则新建分配器,记录到mAllocatorPool。
cpp
// 请求一个命令分配器,参数CompletedFenceValue为当前已完成围栏值
ID3D12CommandAllocator* CommandAllocatorPool::RequestAllocator(uint64_t CompletedFenceValue)
{
// 加锁
std::lock_guard<std::mutex> LockGuard(mAllocatorMutex);
ID3D12CommandAllocator* pAllocator = nullptr;
// 查找就绪分配器队列mReadyAllocators
if (!mReadyAllocators.empty())
{
// 获取就绪队列首个分配器,以及表示其是否使用完成的围栏值
std::pair<uint64_t, ID3D12CommandAllocator*>& AllocatorPair = mReadyAllocators.front();
// 若当前完成围栏值 >= 分配器围栏值,则说明此分配器使用完毕,可复用
if (AllocatorPair.first <= CompletedFenceValue)
{
// 调用Reset重置分配器,将其弹出就绪队列,返回给调用者
pAllocator = AllocatorPair.second;
ASSERT_SUCCEEDED(pAllocator->Reset());
mReadyAllocators.pop();
}
}
// 如果没有可复用分配器,则创建分配器并返回
if (pAllocator == nullptr)
{
ASSERT_SUCCEEDED(mDevice->CreateCommandAllocator(m_cCommandListType, MY_IID_PPV_ARGS(&pAllocator)));
// 创建新的分配器并压入pAllocator,pAllocator是单调递增的
// 使用"CommandAllocator + mAllocatorPool.size()"设置新增分配器的名称
wchar_t AllocatorName[32];
swprintf(AllocatorName, 32, L"CommandAllocator %zu", mAllocatorPool.size());
pAllocator->SetName(AllocatorName);
mAllocatorPool.push_back(pAllocator);
}
return pAllocator;
}
(4) 类总结
- CommandAllocatorPool封装了同类型命令的分配器池,其意义是统一管理同类型的所有命令分配器,以达到最大的内存复用效果。
- CommandAllocatorPool的RequestAllocator和DiscardAllocator接口加了锁,即其是线程安全的。而CommandAllocatorPool、~CommandAllocatorPool、Create、Shutdown、Size未加锁,是非线程安全的。
- 一般构造和Create函数在系统初始化调用,而析构和Shutdown在系统终止时调用,所以这些一般不用考虑线程安全。而仅剩下的Size一般仅作为性能分析使用,一般不会产生大问题。
- 需要注意的是在该类析构或者调用Shutdown前,必须保证池内命令分配器对应的指令必须在GPU执行完毕。该类使用前也必须调用Create。
(5) 最终代码
- CommandAllocatorPool类代码位于:CommandAllocatorPool,可作为独立组件使用。