MiniEngine学习笔记 : CommandAllocatorPool

学习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) 最终代码

相关推荐
一楼的猫7 分钟前
茄子写作助手——品牌搜索突破9万后的技术型品牌认知与官网入口指南
人工智能·学习·机器学习·chatgpt·ai写作
AOwhisky19 分钟前
学习自测与解析:MySQL第五、六、七期核心知识点详解
运维·数据库·笔记·学习·mysql·云计算
niuniuyi~36 分钟前
QT学习笔记
笔记·qt·学习
咸甜适中40 分钟前
rust语言学习笔记Trait(十六)Error(错误)
笔记·学习·rust
xuhaoyu_cpp_java1 小时前
项目学习(三)代码生成器
java·经验分享·笔记·学习
my_daling2 小时前
松下伺服驱动器参数保存流程(已在松下A5上验证)
笔记
worilb2 小时前
Spring Cloud 学习与实践(8):Spring Cloud Gateway 统一入口、路由转发与双重跨域故障演练
学习·spring·spring cloud
初圣魔门首席弟子2 小时前
学习工作方法论与任务执行计划
学习
智者知已应修善业2 小时前
【51单片机初始化D5-D8亮,每按键按下D1到D4全亮,再按下恢复,如此循环】2024-3-26
c++·经验分享·笔记·算法·51单片机
skywalk81632 小时前
记录段言的开发过程
开发语言·学习·编程