在DX12中管理资源的分配

在DX12中管理资源的分配

​ 这几天对着MiniEngine学了一下对DX12底层接口的封装及各种资源的管理,同时也自己试着封装了一下代表显存资源的ID3D12Resource,并对其的分配进行管理。要对资源进行管理首先就需要知道要如何创建这个资源,该接口所表示的资源由设备ID3D12Device创建,在DX12中有三种方式创建该资源,分别是:

  • 在隐式堆中创建资源:该方式对应的接口为ID3D12Device::CreateCommittedResource,使用该方法会创建一个资源,同时也会创建一个大小足以容纳该资源的堆,并将该资源映射到堆中,由于该堆是不可见的,因此也被称为隐式堆。
  • 在指定堆中创建资源:该方法对应的接口为ID3D12Device::CreatePlacedResource,使用该方法之前需要手动创建一个堆,对应的接口为ID3D12Device::CreateHeap,需要的参数即为对堆的类型及标志的描述,创建完堆之后,才可调用上述方法在指定的堆上创建资源,由于不需要专门为该资源创建一个堆,因此通过该接口创建的资源是最轻量级,开销最小的。使用该方法分配的资源可在堆中重叠,但是相互重叠的资源中只有一个能被使用,若想要转换使用的资源,需要使用别名屏障D3D12_RESOURCE_ALIASING_BARRIER 进行转换,通过不同资源的重叠,可以更高效的使用堆的空间。
  • 创建保留资源:该方法对应的接口为ID3D12Device::CreateReservedResource,该方法只会为资源分配一个虚拟地址,并不会将其映射到实际的物理内存中,在使用该资源前需要通过调用 CopyTileMappings或 UpdateTileMappings手动将其映射到实际的物理内存中。一般在空闲的显存不足的时候会使用该方法分配资源,等显存充足的时候再将其映射到实际的内存中,因此该方法使用较少。

了解资源如何分配的之后,便可以着手管理资源的分配了,由上面的描述可以知道,在指定堆上创建资源是最高效,因此我就打算将所有资源的分配都在指定堆中进行,手动创建或销毁堆并进行管理。

对显存资源的简易封装

​ 首先需要对要对ID3D12Resource接口进行简易的封装以便后续的使用,由前面的描述可以得知,要创建一个资源,就需要得知该资源是什么样的,以及在什么堆上创建,因此可以将这些描述结构体封装在一起:

struct GpuResourceDesc
{
    D3D12_HEAP_TYPE m_HeapType = D3D12_HEAP_TYPE_DEFAULT;
    D3D12_HEAP_FLAGS m_HeapFlags = D3D12_HEAP_FLAG_NONE;
    D3D12_RESOURCE_DESC m_Desc{};
    D3D12_RESOURCE_STATES m_State = D3D12_RESOURCE_STATE_COMMON;
};

对资源的封装十分简易,只是添加智能指针对该资源的生命周期进行管理,同时添加该资源当前的状态,以方便对资源的状态进行转换,同时封装了一些常用的函数:

class GpuResource
{
public:
    GpuResource() = default;
    GpuResource(const GpuResourceDesc& resourceDesc){ Create(resourceDesc); }
    ~GpuResource() { Destroy(); }
    GpuResource(GpuResource&& resource) noexcept = default;
    GpuResource& operator=(GpuResource&& resource) noexcept = default;
    DSM_NONCOPYABLE(GpuResource);

    virtual void Create(const GpuResourceDesc& resourceDesc);
    virtual void Destroy();

    ID3D12Resource* operator->() { return m_Resource.Get(); }
    const ID3D12Resource* operator->() const { return m_Resource.Get(); }
    ID3D12Resource** operator&() { return m_Resource.GetAddressOf(); }
    ID3D12Resource* const * operator&() const { return m_Resource.GetAddressOf(); }

    ID3D12Resource* GetResource(){ return m_Resource.Get(); }
    const ID3D12Resource* GetResource() const { return m_Resource.Get(); }
    ID3D12Resource** GetAddressOf() { return m_Resource.GetAddressOf(); }
    ID3D12Resource* const * GetAddressOf() const { return m_Resource.GetAddressOf(); }

    D3D12_RESOURCE_STATES GetUsageState() const noexcept { return m_UsageState; }

    D3D12_GPU_VIRTUAL_ADDRESS GetGpuVirtualAddress() noexcept { return m_Resource->GetGPUVirtualAddress(); }

protected:
    Microsoft::WRL::ComPtr<ID3D12Resource> m_Resource{};
    // 资源当前的状态
    D3D12_RESOURCE_STATES m_UsageState{};

    // 该资源的创建者
    GpuResourceAllocator* m_Allocator{};
};

其中CreateDestroy便是根据资源描述从资源管理者那获取资源。

堆的线性分配管理

​ MiniEngine中实现两种资源的管理方式,一种是Buddy System,一种是Linear Allocate,其中Buddy System并未被实际使用。先前我也仿照着MiniEngine实现了用BuddySystem的分配方案在一个大的资源中分配子资源,因此这次我是用Linear Allocate的方式管理堆内资源的分配。

​ 首先是对线性分配的单独实现,该辅助类需要在初始化的时候决定最大大小和起始偏移,同时每次分配的时候能够进行对齐并返回分配的偏移量:

// 对线性资源进行分配的辅助类
class LinearAllocator
{
public:
    LinearAllocator(std::uint64_t maxSize, std::uint64_t startOffset = 0)
        :m_MaxSize(maxSize), m_StartOffset(startOffset){}
    ~LinearAllocator() = default;

    // 返回分配的资源所处的偏移量
    std::uint64_t Allocate(std::uint64_t size, std::uint32_t alignment = 0) noexcept
    {
        auto alignOffset = Utility::AlignUp(m_CurrOffset, alignment);
        m_CurrOffset = alignOffset + size;
        return (alignOffset + size) > m_MaxSize ? Utility::INVALID_ALLOC_OFFSET : alignOffset;
    }
    void Clear() noexcept
    {
        m_CurrOffset = m_StartOffset;
    }

    bool Full() const noexcept { return m_CurrOffset >= m_MaxSize; }
    bool Empty() const noexcept { return m_CurrOffset == m_StartOffset; }
    std::uint64_t MaxSize() const noexcept { return m_MaxSize; }
    std::uint64_t UsedSize() const noexcept { return m_CurrOffset; }

private:
    const std::uint64_t m_MaxSize{};      // 最大容量
    const std::uint64_t m_StartOffset{};  // 起始偏移
    std::uint64_t m_CurrOffset{};   // 当前的偏移
};   

计算向上对齐的函数如下,听说这个对齐方式曾经还是微软的考试题:

template <typename T> 
inline constexpr T AlignUp( T value, size_t alignment ) noexcept
{
    if (alignment == 0 || alignment == 1) return value;
    else return (T)(((size_t)value + (alignment - 1)) & ~(alignment - 1));
}

分配页的实现

​ 一个分配页包含一个堆,同时还有上述实现的线性管理辅助类。由于只有一个堆中的所有资源都被释放的时候,该堆才能被释放,因此还需要实时记录从该堆上分配的子资源。具体实现如下:

class GpuResourcePage
{
    friend class GpuResourceAllocator;
public:
    GpuResourcePage(ID3D12Heap* agentHeap)
        :m_Heap(agentHeap), m_Allocator(agentHeap->GetDesc().SizeInBytes){}
    ~GpuResourcePage() = default;
    DSM_NONCOPYABLE_NONMOVABLE(GpuResourcePage);

    ID3D12Resource* Allocate(const D3D12_RESOURCE_DESC& resourceDesc, D3D12_RESOURCE_STATES resourceState);
    bool ReleaseResource(ID3D12Resource* resource);
    void Reset() noexcept
    {
        m_SubResources.clear();
        m_Allocator.Clear();
    }
    std::size_t GetSubresourcesCount() const noexcept{ return m_SubResources.size(); }

    bool Full() const noexcept { return m_Allocator.Full(); }
    bool Empty() const noexcept { return m_SubResources.empty(); }

private:
    Microsoft::WRL::ComPtr<ID3D12Heap> m_Heap{};
    std::set<ID3D12Resource*> m_SubResources{};
    LinearAllocator m_Allocator;
};

ID3D12Resource* GpuResourcePage::Allocate(const D3D12_RESOURCE_DESC& resourceDesc, D3D12_RESOURCE_STATES resourceState)
{
    auto resourceSize = resourceDesc.Width * resourceDesc.Height * resourceDesc.DepthOrArraySize;
    auto offset = m_Allocator.Allocate(resourceSize, D3D12_DEFAULT_RESOURCE_PLACEMENT_ALIGNMENT);
    ID3D12Resource* resource = nullptr;
    if (offset != Utility::INVALID_ALLOC_OFFSET) {
        ASSERT_SUCCEEDED(g_RenderContext.GetDevice()->CreatePlacedResource(
            m_Heap.Get(), offset, &resourceDesc,
            resourceState, nullptr, IID_PPV_ARGS(&resource)));
        m_SubResources.insert(resource);
    }
    return resource;
}

bool GpuResourcePage::ReleaseResource(ID3D12Resource* resource)
{
    if (m_SubResources.contains(resource)) {
        m_SubResources.erase(resource);
        return true;
    }
    else{
        return false;
    }
}

由于每个子资源的指针都是唯一的,而且释放资源的时候需要快速查找到该资源,因此使用set来储存子资源的指针,当set为空的时候及表示该堆可以被释放。

posted @ 2025-10-13 23:50  单身喵  阅读(4)  评论(0)    收藏  举报