在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{};
};
其中Create
与Destroy
便是根据资源描述从资源管理者那获取资源。
堆的线性分配管理
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为空的时候及表示该堆可以被释放。