MiniEngine学习笔记 : CommandAllocatorPool
学习CommandAllocatorPool类
- 专栏前言
- CommandAllocatorPool
- (1) 源码展示
- (2) 源码分析
- (3) 类使用分析
- (4) 类总结
- (5) 最终代码
专栏前言
- 最近想要开发一个游戏引擎,使用D3D12作为图形API,于是在看D3D12-MiniEngine。
- MiniEngine是微软DirectX示例库中的一个项目,链接为DirectX-Graphics-Samples。
- 我的初步想法是基于MiniEngine集成一套RenderContext,即渲染上下文,支持完整的渲染工作流。然后在此之上在集成一套RenderGraph,即渲染依赖图,支持各种渲染后处理和优化。
- 本专栏文章主要分析MiniEngine库代码,逐步将其拆解为独立的、完整的组件,为自定义项目开发提供技术支撑。
CommandAllocatorPool
(1) 源码展示
- 类头文件如下:
#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;
};
- 类源文件如下:
#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) 源码分析
类成员变量如下:
// 命令类型(不可修改)
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;
类方法如下:
// 构造函数,记录命令列表类型,初始化设备指针为空
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;// 查找就绪分配器队列mReadyAllocatorsif (!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>压入mReadyAllocatorsstd::lock_guard<std::mutex> LockGuard(mAllocatorMutex);mReadyAllocators.push(std::make_pair(FenceValue, Allocator));
}// 返回池内分配器总数
inline size_t Size() { return mAllocatorPool.size(); }
(3) 类使用分析
- 可以看到CommandAllocatorPool封装了固定命令类型的分配器池,用户通过下面接口获取分配器。其中CompletedFenceValue表示当前完成的围栏值。
ID3D12CommandAllocator* RequestAllocator(uint64_t CompletedFenceValue);
- 用户获取并使用分配器后,通过下面接口归还。mReadyAllocators中记录了归还的分配器,还记录了表示分配器可复用的对应围栏值。
// 释放命令分配器,其中FenceValue代表该命令可复用的围栏值
void CommandAllocatorPool::DiscardAllocator(uint64_t FenceValue, ID3D12CommandAllocator* Allocator)
{// 加锁,并将<FenceValue, Allocator>压入mReadyAllocatorsstd::lock_guard<std::mutex> LockGuard(mAllocatorMutex);// That fence value indicates we are free to reset the allocatormReadyAllocators.push(std::make_pair(FenceValue, Allocator));
}
- 然后再回到获取分配器,其逻辑如下。可以看到若有已完成的就绪分配器,则弹出mReadyAllocators并返回给用户。若没有可复用分配器,则新建分配器,记录到mAllocatorPool。
// 请求一个命令分配器,参数CompletedFenceValue为当前已完成围栏值
ID3D12CommandAllocator* CommandAllocatorPool::RequestAllocator(uint64_t CompletedFenceValue)
{// 加锁std::lock_guard<std::mutex> LockGuard(mAllocatorMutex);ID3D12CommandAllocator* pAllocator = nullptr;// 查找就绪分配器队列mReadyAllocatorsif (!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,可作为独立组件使用。
