🔧

DirectXのメモリ解放忘れを調べるデバック機能の注意点

2023/09/28に公開

はじめに

結論:ComPtrの解放はデストラクタが終わってからなので、それより先に生存状況調べてもそりゃ生きてるよ(ReportLiveDeviceObjectsしても生きてるよ)

この記事では、「DirectX11」と「DirectX12」において、メモリリーク(解放忘れ)が起こった際に使うと便利な機能で、勘違い してしまう仕様があるので記事にしています。
(私&周りの友人が結構引っかかっていたので)

DirectX11では「ID3D11Debug」。DirectX12では「ID3D12DebugDevice」。そして(11でも12でも)ComPtrを使う際の注意点です。一応これらの使いかたを解説してから注意点を書きますが、使い方をわかっている方は以下のページ内リンクよりスキップしてください。

スキップ用

まだまだプログラミング勉強中なので間違った知識を記事にしてしまっていたり、もっと良い書き方があれば修正しますので教えていただけると幸いです。
(優しく教えてくださいm(__)m)
また、この記事は自分がプログラムを初めて1年目の頃でも理解できるような記事を目標に作成したので当たり前のことが書かれているかもしれませんが気にしないでください。

(前提)ComPtrの概要

スマートポインタです。スマートポインタとは、使われなくなったら自動で解放してくれるポインタです。deleteを書くのを忘れて解放し忘れたー(><)ということがなくなります。便利ですね。

c++の標準ライブラリにもスマートポインタがあります。shared_ptrとかですね。ComPtrはそれとは別の種類のスマートポインタです。
ComPtrはMicrosoftさんが用意してくれたスマートポインタです。
DirectXもMicrosoftが作ったものです。
なので、DirectXのオブジェクトはComPtrで作るといろいろ都合がいいんですねこれが。

詳しい説明になると、参照カウンタというのがあってそれが0になると...と長くなるので割愛しますがスマートポインタの知識はいずれ必須になります。スマートポインタの理解がない方は調べてみるのもいいかもしれませんね。

(前提)ID3D11DebugとID3D12DebugDeviceの概要

DirectXのバージョンによって、もちろん使うオブジェクト(クラス)が変わってくるわけですがDebug機能に関してはDirectX11では「ID3D11Debug」。DirectX12においては「ID3D12DebugDevice」を使用します。

両方ともにある機能として、生存中のデバイスオブジェクトの有効期間に関する情報を報告する機能があります。要は、まだ解放されていないDirectXのクラスのオブジェクトを教えてくれるというわけです。

もちろん、プログラムが動いている間は生存しているのですが、終了してからも生存しているとなるのが俗にいうメモリリークです。バグなので絶対にあってはいけません。

DirectXでフレームワーク等作成しているときに起こるメモリリークは、C++のメモリリークと、DirectXのメモリリークがあります。
C++のメモリリークが起きてるかどうかは、「_CrtSetDbgFlag」で確認できます。

メモリリーク確認
int main()
{
    _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
    //↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
    //ここから処理
}

これで、メモリリークが起きた人は出力に表示されるようになったはずです。
↓ new int()をdeleteしなかった人
image.png

C++のメモリリークは、自力で探すしかありません。自分がやらかしているところを探しましょう。
(一応楽に見つけれる方法もありますが、自分で調べてください^^)

DirectXのメモリリークに関しては、DirectXのオブジェクト(Deviceとか、DeviceContextとか)の解放忘れで起こるメモリリークです。
表示させるためには、11ではDeviceを作成するときの第4引数にDebugの設定をすれば。12ではEnableDebugLayerでDebugを有効にすればDirectXのメモリリークを表示できます。

DirectXのメモリリーク確認

//=============================
// 11の場合
//=============================
	UINT flags = 0;
	
	creationFlags |= D3D11_CREATE_DEVICE_DEBUG; 
	
	// デバイスとデバイスコンテキスト作成
	D3D11CreateDevice(nullptr,D3D_DRIVER_TYPE_HARDWARE,nullptr,
	creationFlags,    //←こいつ 
	featureLevels,_countof(featureLevels),D3D11_SDK_VERSION,
	&m_cpDevice,&futureLevel,&m_cpDeviceContext)))

//=============================
// 12の場合
//=============================
	ComPtr<ID3D12Debug> cpDebugLayer = nullptr;
	if (SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(&cpDebugLayer))))
	{
		cpDebugLayer->EnableDebugLayer();
		cpDebugLayer->Release();
	}

これでメモリリークを見ることができます。
↓DirectXのメモリリーク

小さくて見にくいので一行抜粋

ID3D11Debug

詳しく説明してくれてるところがあるのでこちらを参照

https://hakase0274.hatenablog.com/entry/2019/06/27/231745

一応簡潔にまとめると

ID3D11Debug
using Microsoft::WRL::ComPtr;
Comptr<ID3D11Device> m_device;
ComPtr<Id3D11Debug> m_debug;
//------------------->

// Deviceの作成をしたものとする

//<------------------

// ここからID3D11Debugの作成

m_cpDevice->QueryInterface(
    __uuidof(ID3D11Debug),
    reinterpret_cast<void**>(m_debug.GetAddressOf())
);

// 作成終わり

// 使うとき

//簡易版が下のものですが、特に使い道が分からないので必要ないかも
//m_debug->ReportLiveDeviceObjects(D3D11_RLDO_SUMMARY);

//詳細版
m_debug->ReportLiveDeviceObjects(D3D11_RLDO_DETAIL);

出力ではこのように表示されます

IDなんとかみたいな名前がついているのが生きているオブジェクトですね。関数を呼ぶとこのように作ったDirectXのオブジェクトが呼んだ時の状態で全て表示されます。赤線引いてる部分(Refcount)が0なら解放されてます。1以上ならまだ残っています。実行中や、解放する前に関数を呼べばもちろん1以上になるはずです。
1以上の状態でプログラムが終了したらそれはメモリリークです。ちゃんと解放するかComPtrを使いましょう。

ID3D12DebugDevice

この説明見たらわかります

https://hakase0274.hatenablog.com/entry/2019/10/14/200000

こちらも簡潔にまとめます

ID3D12DebugDevice
using Microsoft::WRL::ComPtr;
ComPtr<ID3D12Device> m_device;
ComPtr<ID3D12DebugDevice> m_debugDevice;

//------------------->

// Deviceの作成をしたものとする

//<------------------

m_device->QueryInterface(m_debugDevice.GetAddressOf());]

// 使うとき

//簡易版(使い道不明)
//m_debugDevice->ReportLiveDeviceObjects(D3D12_RLDO_SUMMARY);
//詳細版
m_debugDevice->ReportLiveDeviceObjects(D3D12_RLDO_DETAIL);

11とほぼ同じですね。

そして出力もほぼ同じ

IDなんとかっていうのが全部DirectX12のオブジェクトです。関数を呼ぶとこのように作成したDirectX12のオブジェクトが呼んだ時の状態ですべて表示されます。赤線引いてる部分(Refcount)が0なら解放されてます。1以上ならまだ残っています。実行中や、解放する前に関数を呼べばもちろん1以上になるはずです。
1以上の状態でプログラムが終了したらそれはメモリリークです。ちゃんと解放するかComPtrを使いましょう。
(黄色に1以上が残っています。これはメモリリークに見えます。しかし違います。それがこの記事の本題です。)

上記2つのDebug用オブジェクトはどの種類のオブジェクトがメモリリークしているかまで表示してくれるので、とても便利ですね。

(本題)よく勘違いする仕様

とても便利なDebug用オブジェクトたちですが、よく勘違いする仕様があります。

1.あくまでReportLiveDeviceObjects関数を呼んだ時の状態を表示するだけ

Debug用オブジェクトの説明の時にも説明しましたが、あくまでその時の状態を教えてくれるだけです。WARNINGと出るので「やばい!メモリリークをおこしてしまった!」と見えてしまいがちですが、ReportLiveDeviceObjectsを呼ぶタイミングを間違えているだけかもしれません。
また、単純にRefcountの部分が0なのにWARNINGが出てるからメモリリークだ!!とならないようRefCountの部分はしっかり見ましょう。

2.ComPtrが自動で解放してくれるのは、デストラクタを過ぎてから

これがこの記事で特に伝えたい点なのですが、ComPtrで作成したオブジェクトは確かに自動で解放してくれます。Releaseを書かなくても解放してくれる便利な機能ですが、解放するのはComPtrがあるクラスのデストラクタが過ぎた後です。
(デストラクタ・・・そのクラスが破棄されるときに呼ばれる関数。「~Class()」みたいな感じのやつ)

1と2から、デストラクタでReportLiveDeviceObjectsを呼んでもまだ生きている!

そう、デストラクタ内部でReportLiveDeviceObjectsを呼んでも解放されているかどうかはわからないのです。

image.png

順番のイメージです。この順番では、解放される前の生存オブジェクトが表示されます。その後に解放されます。
そういう使いかたをしたい方はOKですが(いつ使うんだろう?)、これで「うわーComPtr使ってる部分でメモリリーク起きたよーなにがスマートなんだよー」というのは見当違いの可能性がある、というお話でした。

Debug用オブジェクトを有効活用するためには、先にDirectXのオブジェクトを管理しているクラスを明示的に解放してあげて、そのあとでReportLiveDeviceObjectsを呼んであげないと見当違いの生存オブジェクトが表示されていまうかもしれません。
(↓ちなみに、DirectX12のDebugDeviceの説明時の黄色のメモリリーク?はこれが原因です)
image.png
これを表示した後にComPtrが解放してくれているので、メモリリークは起こってないけどこういう表示になっていました。

3.SUMMARYで調べた時に出てくるもの

image.png
Debugオブジェクトの説明をした際に、簡易版を使う理由が分からないと書きましたが本当にわかりません。
これ、全部解放しててこの表記なのでこの数字の意味が分からないです。有識者の方教えてください。
これを見て「Wow.This is memoryleak」とはならずに、DETAILでも一度調べてみましょう。実は解放されているかもしれないので。
DETAILでも残っていればメモリリークかもしれないですね...。

特に管理クラスをシングルトンで作っている人

DirectXのオブジェクトを管理しているクラスをシングルトンで作る人も多いと思います(私はそうしています)。アクセスしやすいですし、2個以上作ることもまぁないと思いますので悪くはないと思っています。
(シングルトン・・・デザインパターンの一つでクラスのインスタンス(実体)の数を1つに保障させることで、グローバル変数のような扱いができるようになる。
ちなみにデザインパターンとは、よく使用される設計をパターン化したもの。簡単に説明すると、「その書き方(設計)、頭いいな!」シリーズ)

しかし、ここで注意点が一つ
実体の参照を返しているシングルトンなどであれば、deleteなどで明示的に解放などはできないので解放は自然と一番最後の方になります。
そうなると、ReportLiveDeviceObjectsを効果的に使えるタイミングがなくなってしまいますね。ComPtrが解放されるのがデストラクタの後なのでデストラクタ内では書けませんし、参照のシングルトンの場合先ほども述べた通り先に解放することができないので、タイミングがなくなってしまいReportLiveDeviceObjectsが使えないということになってしまいます。残念。

これに関しては、シングルトンの方法を変えるかシングルトンをやめるぐらいしか解決方法はないと思いますので解決はしなくてもいいと思いますが、この記事のような勘違いがあるかもしれないので、頭の片隅に置いておくといいかもしれませんね。

引用

https://hakase0274.hatenablog.com/entry/2019/06/27/231745

https://hakase0274.hatenablog.com/entry/2019/10/14/200000

神戸電子専門学校ゲーム技術研究部

Discussion