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

相关推荐
kblj55553 小时前
学习Linux——网络——网卡
linux·网络·学习
暖阳之下3 小时前
学习周报二十
人工智能·深度学习·学习
朝新_3 小时前
【SpringBoot】玩转 Spring Boot 日志:级别划分、持久化、格式配置及 Lombok 简化使用
java·spring boot·笔记·后端·spring·javaee
charlie1145141913 小时前
CSS学习笔记3:颜色、字体与文本属性基础
css·笔记·学习·教程·基础
wangqiaowq4 小时前
PAIMON+STARROCKS 学习
学习
phoenix09815 小时前
ELK企业级日志分析系统学习
学习·elk
奋斗的牛马5 小时前
FPGA—ZYNQ学习GPIO-EMIO,MIO,AXIGPIO(五)
单片机·嵌入式硬件·学习·fpga开发·信息与通信
ACGkaka_5 小时前
设计模式学习(十二)状态模式
学习·设计模式·状态模式
TheInk5 小时前
python学习笔记之Python基础教程(crossin全60课)
笔记·python·学习