はかせのラボ

私の頭の中を書いていく雑記ブログです

DirectX12 ComPtrに直接値を代入したりするのは危険だっていう話

あいさつ

どうも、はかせです。

前回ID3D12DebugDeviceを使って
どのオブジェクトが破棄されていないのかを確認しました。
hakase0274.hatenablog.com

今回はその内容の確認と実際に
メモリを解放しメモリリークを解決した話です。

リークしていたメモリ

今回リークしていたのは
・各種テクスチャ
・テクスチャに紐づくヒープ
・RootSignature
・Fence
・CommandQueue
・それらに紐づく各種リソース

以上です。

上二つは予想通りでしたが、
RootSignature以下がなぜって感じですね。

それぞれしっかり撲滅解放していきましょう。

テクスチャ周りの解放

「解放されてないんだったらデストラクタで解放すればいいじゃない。」
そう考えていた時期もありました。

ただデストラクタで解放すると
デストラクタでの解放+ComPtrのデストラクタによる解放が発生して
二重Releaseになって怒られるんですよね。

ここからわかるのはReleaseされているが
メモリが解放されていないということです。

つまり何らかの要因によってCOMオブジェクトのポインタの管理が
ComPtrから外れてしまっているわけですね。

ではなぜComPtrからポインタが外れてしまったのか。
その答えは演算子オーバーロードです。

これは私がテクスチャを生成し配列に格納するまでの
流れのダイジェスト的コードです。

std::vector<ComPtr<ID3D12Resource>> mTextureList;
//予め使う分の領域を確保しておく
mTextureList.resize(mTextureFileList.size());
/*
テクスチャ読込
よくあるファイル読み込んでその数だけループする奴
今回は処理の一部のみ記載
*/
ID3D12Resource* texture;
//WICを使ってテクスチャを読み込む
std::unique_ptr<uint8_t[]> wicData;
D3D12_SUBRESOURCE_DATA subresouceData;
ThrowFailed(LoadWICTextureFromFile(
	mDevice.Get(),
	mPMDLoader.mTextureNameList[i].c_str(),
	&texture,
	wicData,
	subresouceData));

D3D12_RESOURCE_DESC textureDesc = texture->GetDesc();
const UINT64 uploadBufferSize = GetRequiredIntermediateSize(texture, 0, 1);
/*
以後なんか続く
*/
//問題の場所
mTextureList[i] = texture;

よくある配列用意して生成したものを都度
配列の中に格納していくやつです。
一見何の問題もなさそうな健全な雰囲気を醸しています。
ですがこのコード実のところ全く健全ではありません
(ムッツリスケベ)

健全ではないのはここです。

//問題の場所
mTextureList[i] = texture;

ComPtrに直接値を代入しています。
実際このComPtrには特別値は入っていないんですが、
vector::resizeで何らかのメモリが確保されています。
(vectorは連続したメモリであることを保証するため)

そしてComPtrの=演算子での動きはSwapです。
つまりこの何らかのデータと
生成したテクスチャのデータを入れ替えています。
テクスチャデータはComPtrの中に入りますが、
何らかのデータはローカルの変数ポインタに行きます。

そしてComPtrのデータなんで
中身はもちろんテンプレートで指定した
データ型のポインタです。

そしてさきほど上げていた
ローカルの変数ポインタは
生ポインタです。

生ポインタに何らかのデータ型のポインタが入り
それがdeleteされることなくスコープを抜けました

はい、これが今回のテクスチャのメモリリークです。

紐づくヒープがあったのはおそらく
内部的にID3D12Resourceを作成したときに
勝手に作られるからでしょう。
(libファイルの中身までは追えてないからわからない)

解決方法はいたってシンプルです。
要はComPtrにSwapでポインタ入れてるのが
悪いんでそれをやめればいいです。

というかそもそもresizeで領域と
紐づくポインタを既に作ってるんだから
それを使えばいいです。

要はこんな感じ

std::vector<ComPtr<ID3D12Resource>> mTextureList;
//予め使う分の領域を確保しておく
mTextureList.resize(mTextureFileList.size());
/*
テクスチャ読込
よくあるファイル読み込んでその数だけループする奴
今回は処理の一部のみ記載
*/
//WICを使ってテクスチャを読み込む
std::unique_ptr<uint8_t[]> wicData;
D3D12_SUBRESOURCE_DATA subresouceData;
ThrowFailed(LoadWICTextureFromFile(
	mDevice.Get(),
	mPMDLoader.mTextureNameList[i].c_str(),
	mTextureList[i].GetAddressOf(),
	wicData,
	subresouceData));

D3D12_RESOURCE_DESC textureDesc = mTextureList[i]->GetDesc();
const UINT64 uploadBufferSize = GetRequiredIntermediateSize(mTextureList[i].Get(), 0, 1);
/*
以後なんか続く
*/

さっきのコードではわざわざ
ローカルに変数を持っていたのをやめて
直接配列の中身を使いました。

RootSignature他の解放

さてテクスチャが解放できたので
謎のRootSignatureその他の解放をしていきます。

といっても本当こいつら何でいるのって感じなんですよね。
元々私が作ったRootSignatureは別であって
そいつは無事解放されているんですよ。
他の変数たちも同様。

こいつらは一体どこで生まれた変数なのか私は全く知りません。
気分はまったく見知らぬ夫婦の子供の
養育費を払えって言われている気分です。

ですが身に覚えがない出費が発生することは
プログラムの世界線上ではあり得ません。
つまり覚えてないだけでどこかでヤってしまったわけですね。

現実世界ではやっちゃったことはきちんと責任取って
やらなければなりませんが、
プログラムの世界ならばやったことをなかったことにできます。

要はやってしまっている該当コードを削除するなり
コストが発生しないよう書き換えればいいわけですね。

では犯人探しと行きましょう。
基本はエラー起こさない程度にコードをコメントアウトしていって
メモリリークの有無を調べていきます。

この作業は地味極まりないので
ここでは割愛します。


・・・数十分後・・・


容疑者が出てきました。
FenceEventです。

厳密に言うとFenceEventを作る
CreateFenceです。

こいつがCommandListを作る時と
CommandQueueを作る時にそれぞれ呼ばれていました。

しかも作られたFenceEventは同一変数に格納されています。
(こいつはくせぇッー!
メモリリークのにおいがプンプンするぜッーーーッ!)

試しに片方を消すと
ものの見事にメモリリークがなくなりました。

FenceEventって作る時にいっぱい
内部変数を作るんですねー

あとがき

今回はメモリリーク撲滅回でした。

DirectX12ってメモリ管理やらなんやらをほぼ100%
プログラマーに委任してパフォーマンスを
追求したものという認識だったんですが、
実際プロファイラーとか見ていくと
結構中身で作ったり破棄したりしているんですね。

あとはスマートポインタの動き。
完全手作業でなくなる分楽なんですが、
知らんと変な沼にハマりますね。

今回の記事良ければスターやコメント等よろしくお願いします。
それでは今回はこの辺でノシ