個人的なDirectX12のディスクリプタ管理方法について

個人的なDirectX12のディスクリプタ管理方法について_アイキャッチ
目次

はじめに

DirectX12を参考書などでサンプルを真似して作りながら勉強してやっとポリゴンやモデルが描画できるようなったり、シェーダーを使って見た目を良くしたりしてある程度DirectX12のことがなんとなく理解できたところで簡単なミニゲームを作ってみようとしたところで自分はリソース管理に悩まされました。

参考書のサンプルなどはモデルの描画やアニメーションの再生など目的を達成するための最低限のことしか書かれていないので例えばモデルを1つ描画するなら定数バッファやテクスチャを必要な数だけ作成して設定すればいいので敵を出現させたりなどゲーム中にリソースの作成、解放など管理をする必要がありません。もしくはそのまま実装しても動かないこともあります。実際にちゃんとしたゲームを作るのであれば様々なリソースがいつどれだけ必要になるかは分かりません。

今回はDirectX12において現時点で自分が使っているDescriptorHeap関連の管理方法を紹介します。

管理方法

まず自分が使っているDescriptorHeapのソースコードの一部をそのまま載せます。これでなんとなく全体像を把握してもらえればと思います。

/// <summary>
/// グローバルディスクリプタヒープ(View置き場)
/// </summary>
class GlobalDescriptorHeap final
{
public:
	HRESULT Create(ID3D12Device8* pDevice);

	void CreateRTV(ID3D12Resource* pResource, std::uint64_t resID);
	//void CreateDSV(ID3D12Resource* pResource);
	void CreateCBV(ID3D12Resource* pResource, std::uint64_t resID);
	void CreateSRV(ID3D12Resource* pResource, std::uint64_t resID);
	//void CreateUAV(ID3D12Resource* pResource);
	ID3D12DescriptorHeap* GetRTVHeap() const { return m_pRTVHeap.Get(); }
	ID3D12DescriptorHeap* GetDSVHeap() const { return m_pDSVHeap.Get(); }
	ID3D12DescriptorHeap* GetCBVSRVUAVHeap() const { return m_pCBVSRVUAVHeap.Get(); }
	ID3D12DescriptorHeap* GetSamplerHeap() const { return m_pSamplerHeap.Get(); }

	D3D12_CPU_DESCRIPTOR_HANDLE GetViewHandle(std::uint64_t resID, HeapType type)
	{
		D3D12_CPU_DESCRIPTOR_HANDLE handle;
		handle.ptr = m_viewTable[static_cast<UINT>(type)][resID];
		return handle;
	}

private:
	GlobalDescriptorHeap() = default;
	~GlobalDescriptorHeap() = default;

private:
	ID3D12Device* m_pDevice;
	ComPtr<ID3D12DescriptorHeap> m_pCBVSRVUAVHeap;
	ComPtr<ID3D12DescriptorHeap> m_pSamplerHeap;// 同じものがあれば使い回す
	ComPtr<ID3D12DescriptorHeap> m_pRTVHeap;
	ComPtr<ID3D12DescriptorHeap> m_pDSVHeap;

	int rtvUsecount = 0;
	UINT m_useCountCBVSRVUAV = 0;

	std::unordered_map<std::uint64_t, size_t> m_viewTable[static_cast<size_t>(HeapType::MAX)];

	/*UINT m_uIncrementSize[static_cast<UINT>(HeapType::MAX)];
	size_t m_nextRegistPtr;*/

public:
	static GlobalDescriptorHeap& Instance()
	{
		static GlobalDescriptorHeap instance;
		return instance;
	}
};

inline void CopyDescriptorGlobalToLocal(ID3D12Device8* pDevice,
	D3D12_CPU_DESCRIPTOR_HANDLE localHeapHandle, std::uint64_t resID, HeapType type)
{
	D3D12_CPU_DESCRIPTOR_HANDLE handle = GlobalDescriptorHeap::Instance().GetViewHandle(resID, type);
	if (handle.ptr == 0) Neko::Core::Assert("取得したいリソースのビューがありません : " + std::to_string(resID));
	pDevice->CopyDescriptorsSimple(1u, localHeapHandle, handle, static_cast<D3D12_DESCRIPTOR_HEAP_TYPE>(type));
}
HRESULT GlobalDescriptorHeap::Create(ID3D12Device8* pDevice)
{
	HRESULT hr = S_OK;
	D3D12_DESCRIPTOR_HEAP_DESC heapDesc = {};
	// ディスクリプタヒープ作成
	// サンプラー
	heapDesc.NumDescriptors = static_cast<UINT>(DescriptorMaxSize::SAMPLER);
	heapDesc.NodeMask = 0;
	heapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;// シェーダーが参照しない
	heapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER;
	hr = pDevice->CreateDescriptorHeap(&heapDesc, IID_PPV_ARGS(m_pSamplerHeap.ReleaseAndGetAddressOf()));
	if (FAILED(hr)) Neko::Core::Assert("SAMPLERディスクリプタヒープの作成に失敗しました");

	// CBVSRVUAV
	heapDesc.NumDescriptors =
		static_cast<UINT>(DescriptorMaxSize::CBV) +
		static_cast<UINT>(DescriptorMaxSize::SRV) +
		static_cast<UINT>(DescriptorMaxSize::UAV);
	heapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
	hr = pDevice->CreateDescriptorHeap(&heapDesc, IID_PPV_ARGS(m_pCBVSRVUAVHeap.ReleaseAndGetAddressOf()));
	if (FAILED(hr)) Neko::Core::Assert("CBVSRVUAVディスクリプタヒープの作成に失敗しました");

	// RTV
	heapDesc.NumDescriptors = static_cast<UINT>(DescriptorMaxSize::RTV);
	heapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
	//m_upRTVHeap = std::make_unique<RTVDescriptorHeap>();
	/*heapDesc.NumDescriptors = 128u;
	heapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;*/
	hr = pDevice->CreateDescriptorHeap(&heapDesc, IID_PPV_ARGS(m_pRTVHeap.ReleaseAndGetAddressOf()));
	if (FAILED(hr)) Neko::Core::Assert("RTVディスクリプタヒープの作成に失敗しました");

	// DSV
	heapDesc.NumDescriptors = static_cast<UINT>(DescriptorMaxSize::DSV);
	heapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV;
	hr = pDevice->CreateDescriptorHeap(&heapDesc, IID_PPV_ARGS(m_pDSVHeap.ReleaseAndGetAddressOf()));
	if (FAILED(hr)) Neko::Core::Assert("DSVディスクリプタヒープの作成に失敗しました");

	/*for (int i = 0; i < static_cast<int>(HeapType::MAX); ++i)
	{
		m_uIncrementSize[i] = m_pDevice->GetDescriptorHandleIncrementSize(static_cast<D3D12_DESCRIPTOR_HEAP_TYPE>(i));
	}*/

	m_pDevice = pDevice;
	return hr;
}

void GlobalDescriptorHeap::CreateRTV(ID3D12Resource* pResource, std::uint64_t resID)
{
	D3D12_CPU_DESCRIPTOR_HANDLE handle = m_pRTVHeap->GetCPUDescriptorHandleForHeapStart();
	D3D12_RENDER_TARGET_VIEW_DESC rtvDesc = {};
	rtvDesc.Format = pResource->GetDesc().Format;
	rtvDesc.ViewDimension = D3D12_RTV_DIMENSION_TEXTURE2D;
	handle.ptr += m_pDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV) * rtvUsecount;
	m_pDevice->CreateRenderTargetView(pResource, &rtvDesc, handle);
	rtvUsecount++;

	m_viewTable[static_cast<UINT>(HeapType::RTV)][resID] = handle.ptr;
}

void GlobalDescriptorHeap::CreateSRV(ID3D12Resource* pResource, std::uint64_t resID)
{
	//通常テクスチャビュー設定
	D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {
		.Format = pResource->GetDesc().Format,
		.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D,
		.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING,
	};
	srvDesc.Texture2D.MipLevels = pResource->GetDesc().MipLevels;
	if (srvDesc.Format == DXGI_FORMAT_R32_TYPELESS)
	{
		srvDesc.Format = DXGI_FORMAT_R32_FLOAT;
	}
	// ビューの作成
	D3D12_CPU_DESCRIPTOR_HANDLE handle = m_pCBVSRVUAVHeap->GetCPUDescriptorHandleForHeapStart();
	UINT uIncrementSize = GraphicsDevice::Instance().GetCBVSRVUAVIncrementSize();
	handle.ptr += static_cast<size_t>(uIncrementSize * m_useCountCBVSRVUAV);
	m_pDevice->CreateShaderResourceView(pResource, &srvDesc, handle);
	m_useCountCBVSRVUAV++;
	// 作成したビューをテーブルに設定する
	//std::uint64_t id = std::hash<std::string_view>()(name);
	m_viewTable[static_cast<UINT>(HeapType::CBVSRVUAV)][resID] = handle.ptr;
}

void GlobalDescriptorHeap::CreateCBV(ID3D12Resource* pResource, std::uint64_t resID)
{
	D3D12_CONSTANT_BUFFER_VIEW_DESC cbView = {};
	cbView.BufferLocation = pResource->GetGPUVirtualAddress();
	cbView.SizeInBytes = pResource->GetDesc().Width;
	// ビューの作成
	D3D12_CPU_DESCRIPTOR_HANDLE handle = m_pCBVSRVUAVHeap->GetCPUDescriptorHandleForHeapStart();
	UINT uIncrementSize = GraphicsDevice::Instance().GetCBVSRVUAVIncrementSize();
	handle.ptr += static_cast<size_t>(uIncrementSize * m_useCountCBVSRVUAV);
	m_pDevice->CreateConstantBufferView(&cbView, handle);
	m_useCountCBVSRVUAV++;
	// 作成したビューをテーブルに設定する
	m_viewTable[static_cast<UINT>(HeapType::CBVSRVUAV)][resID] = handle.ptr;
}

管理方法としては定数バッファやテクスチャを作成したときにViewも作成するのですがそのViewの作成と管理をグローバルディスクリプタヒープに任せます。このグローバルディスクリプタヒープはコマンドリストに設定はしません。管理するだけです。そして実際にレンダリングで使用するときに必要なViewを検索して使用するディスクリプタヒープに設定していきます。

まずはグローバルディスクリプタヒープの作成です。サンプラーステートの作成を例に説明するとサンプラーを管理する数やシェーダーが参照するかなどを設定して作成するだけです。これをSRVCBVUAV、RTV、DSVなど管理したいViewの種類の数だけ作成します。NumDescriptorの数は今のところすべて128にっています。これに関しては既存のゲームエンジンで設定されている数を設定してあげるのがいいと思います。

// サンプラー
	heapDesc.NumDescriptors = static_cast<UINT>(DescriptorMaxSize::SAMPLER);
	heapDesc.NodeMask = 0;
	heapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;// シェーダーが参照しない
	heapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER;
	hr = pDevice->CreateDescriptorHeap(&heapDesc, IID_PPV_ARGS(m_pSamplerHeap.ReleaseAndGetAddressOf()));
	if (FAILED(hr)) Neko::Core::Assert("SAMPLERディスクリプタヒープの作成に失敗しました");

次にViewの作成についてです。今回はRTVで説明します。やっていることは参考書のサンプルとほぼ変わりませんが、最後のViewTableにhandle.ptrを格納しています。このviewTableはRTV,DSVなどViewの種類分を配列で作成しています。unordered_mapでhandle.ptrとリソースIDを紐付けしています。

void GlobalDescriptorHeap::CreateRTV(ID3D12Resource* pResource, std::uint64_t resID)
{
	D3D12_CPU_DESCRIPTOR_HANDLE handle = m_pRTVHeap->GetCPUDescriptorHandleForHeapStart();
	D3D12_RENDER_TARGET_VIEW_DESC rtvDesc = {};
	rtvDesc.Format = pResource->GetDesc().Format;
	rtvDesc.ViewDimension = D3D12_RTV_DIMENSION_TEXTURE2D;
	handle.ptr += m_pDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV) * rtvUsecount;
	m_pDevice->CreateRenderTargetView(pResource, &rtvDesc, handle);
	rtvUsecount++;

	m_viewTable[static_cast<UINT>(HeapType::RTV)][resID] = handle.ptr;
}

最後にViewの検索とディスクリプタヒープへの設定です。これはグローバルディスクリプタヒープからリソースIDを使用して検索をかけて目的のViewのポインタを取得します。取得したものを引数のlocalHeapHandleにCopyDescriptorsSimple関数を使用してコピーします。最初はリソース名(string型の文字列)から検索していましたが、一文字間違えるだけでリソースが取得できないのと文字列の検索は基本遅いので管理するViewが増えるほど実行速度に影響が出そうなのでID(uint64)に変更しました。

D3D12_CPU_DESCRIPTOR_HANDLE GetViewHandle(std::uint64_t resID, HeapType type)
	{
		D3D12_CPU_DESCRIPTOR_HANDLE handle;
		handle.ptr = m_viewTable[static_cast<UINT>(type)][resID];
		return handle;
	}

inline void CopyDescriptorGlobalToLocal(ID3D12Device8* pDevice,
	D3D12_CPU_DESCRIPTOR_HANDLE localHeapHandle, std::uint64_t resID, HeapType type)
{
	D3D12_CPU_DESCRIPTOR_HANDLE handle = GlobalDescriptorHeap::Instance().GetViewHandle(resID, type);
	if (handle.ptr == 0) Neko::Core::Assert("取得したいリソースのビューがありません : " + std::to_string(resID));
	pDevice->CopyDescriptorsSimple(1u, localHeapHandle, handle, static_cast<D3D12_DESCRIPTOR_HEAP_TYPE>(type));
}

まとめ

以上が自分のリソースの管理方法にについてです。参考になりましたでしょうか?こういったリソースの管理については悩みが尽きることはないので程々にしてゲーム開発のほうを進めていきましょう。ゲームを作っているともっとこうした方が良いなどアイデアが浮かぶと思うのでそうなったらまた管理方法を改善していきましょう。

目次