はじめに
以前にインターンシップに参加させていただいた際に、「FlyweightパターンはDirectXでも大きな効果を発揮する」と言っていたのを思い出したので試してみました。実際に10個のオブジェクト描画に使用するメモリを節約するためにFlyweightパターンを使用しました。
この記事の対象者
- Direct12でFlyweightパターンの効果があるのかを実装前に知りたい方
- Direct12の基本的な描画処理ができる方
- C++の基本文法が理解できる方
- PBRの基礎知識のある方
結果
結果から先に書いておきます。
上:Flyweightパターン適応前のメモリ使用率
下:Flyweightパターン適応後のメモリ使用率
メモリ使用率はVisualStudio2019のメモリプロファイラを使用して計測してます。
この各山がティーポットの初期化によるメモリ使用率を表しています。適応前はメモリ使用率の山が10個あるのが見てわかります。しかし、Flyweightパターン適応後は、山が1つになっています。これは、Flyweightパターンによってリソースを共有したことにより1個分のメモリを使用して10個のティーポットを描画しているからです。
以下が実際の出力になります。出力は適応前と後で変化はありません。
※)初期化後にメモリ使用率が下がるのは、CPUとGPUのレジスタにハンドルを渡した時点で、メモリ側のリソースは破棄しているからだと思います。(Direct12仕様を調べたりしましたが、明確な答えはわかりません。もし、わかる方がいましたらコメントいただけると嬉しいです。)
Flyweightパターン適応前のコード
class Object
{
public:
std::vector<Mesh*> m_pMesh; //メッシュ
Material m_Material; // マテリアル
//初期化処理
bool OnInit(ComPtr<ID3D12Device> m_pDevice, DescriptorPool* m_pPool, ID3D12CommandQueue* m_pQueue);
//描画処理
void Render(ID3D12GraphicsCommandList* pCmd);
};
これはティーポットを描画するためのクラスのヘッダーファイルとなっています。(本記事で使用する変数と関数に留めています)メッシュとマテリアルが主にメモリサイズが大きいと考えたので、今回はこのクラスごとFlyweightパターンで共有しました。
m_Teapot_0.OnInit(m_pDevice, m_pPool[POOL_TYPE_RES], m_pQueue.Get());
m_Teapot_1.OnInit(m_pDevice, m_pPool[POOL_TYPE_RES], m_pQueue.Get());
m_Teapot_2.OnInit(m_pDevice, m_pPool[POOL_TYPE_RES], m_pQueue.Get());
m_Teapot_3.OnInit(m_pDevice, m_pPool[POOL_TYPE_RES], m_pQueue.Get());
m_Teapot_4.OnInit(m_pDevice, m_pPool[POOL_TYPE_RES], m_pQueue.Get());
m_Teapot_5.OnInit(m_pDevice, m_pPool[POOL_TYPE_RES], m_pQueue.Get());
m_Teapot_6.OnInit(m_pDevice, m_pPool[POOL_TYPE_RES], m_pQueue.Get());
m_Teapot_7.OnInit(m_pDevice, m_pPool[POOL_TYPE_RES], m_pQueue.Get());
m_Teapot_8.OnInit(m_pDevice, m_pPool[POOL_TYPE_RES], m_pQueue.Get());
m_Teapot_9.OnInit(m_pDevice, m_pPool[POOL_TYPE_RES], m_pQueue.Get());
そして、これが実際の初期化処理になっています。このコードでは、同じティーポット(メッシュとマテリアル)を10個生成してしまうため、9個分は無駄なメモリを使用してしまいます。
Flyweightパターン適応後のコード
#pragma once
#include "Object.h"
using namespace DirectX::SimpleMath;
class Object_FlyweightFactory
{
public:
Object_FlyweightFactory();//コンストラクタ
virtual ~Object_FlyweightFactory();//デストラクタ
//オブジェクトプール
std::map<int,Object*> m_Object_Pool;
//オブジェクトプールの検索とオブジェクトの初期化
Object* get(int object_id, ComPtr<ID3D12Device> m_pDevice, DescriptorPool* m_pPool, ID3D12CommandQueue* m_pQueue) ;
size_t getPoolSize();
};
これはFlyweightFactoryクラスのヘッダーファイルです。m_Object_Poolでティーポットの管理をします。get()の中で、object_idによりm_Object_Poolの中を検索して、ない場合にはオブジェクト生成と初期化します。
#include "Object_FlyweightFactory.h"
using namespace DirectX::SimpleMath;
//コンストラクタ
Object_FlyweightFactory::Object_FlyweightFactory() {}
//デストラクタ
Object_FlyweightFactory::~Object_FlyweightFactory() {
m_Object_Pool.clear();
std::map<int, Object*>(m_Object_Pool).swap(m_Object_Pool);
}
//オブジェクトプールの検索とオブジェクトの初期化
Object* Object_FlyweightFactory::get(int object_id,ComPtr<ID3D12Device> pDevice, DescriptorPool* pPool, ID3D12CommandQueue* pQueue){
auto search_result = m_Object_Pool.find(object_id);// id検索
if (search_result == end(m_Object_Pool)) {//プール内にない時
switch (object_id) {
case 1: // tea_pod
Object* object = new Object();
object->OnInit(pDevice, pPool, pQueue);//リソース初期化
m_Object_Pool.insert(std::make_pair(object_id, object));
break;
}
}
return m_Object_Pool[object_id] ;
}
size_t Object_FlyweightFactory::getPoolSize() {
return m_Object_Pool.size();
}
これはFlyweightFactoryクラスのソースファイルです。オブジェクトプールにティーポットIDがある場合は、オブジェクトプール内にあるリソースを返します。ない時は、そのオブジェクトを生成してオブジェクトプールに入れ込みます。
まとめ
Flyweightパターンを使用してメモリ使用率を最適化しました。その結果,Visual Studioのメモリプロファイラによって、メモリの使用量の減少を確認できました。
今後の課題
本記事のFlyweightパターンは、メッシュとマテリアルを共通化する事でメモリの節約を行いました。今回はObjectクラスをすべて共通化しました。しかし、実際は、Objectクラス内に定数バッファ(変換行列)をメンバ変数として持っているため、汎用的な設計にはできませんでした。あらためて、汎用性と最適化の両立は難しいですね。
また、今回はCPU、GPUの処理時間やVRAMなどを考えておらず、実際のエンジンでレベルでは、全てのパフォーマンスを考えて設計、構築する必要があります。