末端のクソブログ

末端のクソブログ

クソブログ の 末端 にある クソブログ へ ようこそ

2023.9.2(dxcompiler.libでシェーダーコンパイルしてPIXでシェーダーデバッグ)

つくってるやつ

DXCompilerでシェーダーコンパイルしろ!

しろとは言わないけど新しいコンパイラのほうがいいじゃん!やりましょう。

します。

今まではfxc.exeとかc++上ではD3DCompileどうとかみたいな関数でやってましたね。しかしこれではシェーダーの申し子ことメッシュシェーダー先輩やナウいDXR関連のシェーダーとかが扱えなかったりします。そこでDXCompilerの出番です。dxc.exeという名前でたぶんDirectXSDKとかgithub にあります。これを使ってdxc.exe ahoshader.hlsl -E bakaEntryPoint ,,,とすればコンパイルできますがやです。めんどくさいです。

ここで賢いあなたは「VisualStudioが勝手にやってくれないかな~」と思うかもしれませんがやってくれます。優秀さが憎いです。ちなみにその方法は書きません。僕と一緒にC++からコンパイルしましょう。利点としてはシェーダーバイナリにどの情報を含ませるか、とかが柔軟に選べます。ちなみにVisual Studioでしか開発しないなら大したメリットはありません。ではやっていきましょう!

準備

多分あなたのつよつよPCにはDirectX12Ultimateとともにdxc.exeがあるとは思いますがまあそういわずに最新リリースを取りに行きましょう!ここ)

中身はbinincludelibフォルダがあります。ここでbinフォルダを見ないでください。なんか便利なexeがいろいろ入っていて(あれ?なんでC++からコンパイルしなくちゃいけないんだ……?)という気持ちになります。そういう気持ちになる前にC++である程度実装してコンコルド効果によって後戻りできない状況を作っておきましょう。

そうしたら'include'フォルダをVSの追加のincludeファイルに入れたり、libフォルダを追加のライブラリディレクトリに入れたり、pragma comment OR追加の依存ファイルにdxcompiler.libを入れておきましょう。

先ほどの一文で環境構築の説明は完璧ですね。

実装

とりあえずincludeするもの

#include "dxc/dxcapi.h"//コンパイル関数とかが入っている
#include "dxc/DxilContainer/DxilContainer.h"//シェーダーバイナリの中身をいじいじできる

いったん作っておくもの

ComPtr<IDxcUtils> m_pUtils;//便利なオブジェクトをいろいろ作れる
ComPtr<IDxcCompiler3> m_pCompiler;//コンパイルする人
ComPtr<IDxcContainerReflection> m_pContainerRefl;//ShaderBinaryの中身をいじいじできる

//HRESULT型のチェックもしてるよ。ただこの段階で失敗するのはたぶんメモリが枯れたとかくらいしかないと思うよ。
auto hr = ::DxcCreateInstance(CLSID_DxcUtils, IID_PPV_ARGS(&m_pUtils));
RETURNIFFAILED(hr);
hr = ::DxcCreateInstance(CLSID_DxcCompiler, IID_PPV_ARGS(&m_pCompiler));
RETURNIFFAILED(hr);
hr = ::DxcCreateInstance(CLSID_DxcContainerReflection, IID_PPV_ARGS(&m_pContainerRefl));
RETURNIFFAILED(hr);

シェーダーをコンパイルしましょう

リファレンス

HRESULT Compile(
  const DxcBuffer    *pSource,
  LPCWSTR            *pArguments,
  UINT32             argCount,
  IDxcIncludeHandler *pIncludeHandler,
  REFIID             riid,
  LPVOID             *ppResult
);

まあリファレンスみればわかるのですが直接dxc.exeのコマンドライン引数を指定してる感じです。あとIncludeHandlerとかいう謎存在がいますね。あとpSourceとかはIDxcUtilsとかを使えばファイルから読み込めます。

ではコマンドライン引数から見ていきましょう。

コマンドライン引数について

ここにすべてが書いてます。自分が使ったものだけ書いていきます。

-I /include/dir/

fxc.exeの時はD3DCompileFromFile関数とかでソースファイルからの相対パスでのincludeは標準のIncludeHandlerで勝手にやってくれました。ただdxcではファイルを指定したりしないのでincludeを探すディレクトリは設定しておかなければなりません。なので-I [ソースファイルがあるディレクトリ]と指定しておくわけです。

ちなみにCompile関数にはLPCWSTRの配列として引数を渡すのですが、-I [インクルードパス]というように二つの要素で渡さなければなりません。

-E [EntryPoint], -T [ShaderModel名]

まあ見たまんまですね。EntryPointは指定しないとmainになるとかどっかで見たような気がする。

デバッグ関連

というわけなのですがデバッグ情報はシェーダーバイナリに含めるか、外部ファイルに出力するか選べます。含める場合は-Qembed_debugで、出力するなら-Fd 出力パス名となります。ところで分けて出力してもらうのはゆとりだと思いませんか?ここはなぜか-Fdでうまくいかなかったから-Qembed_debugコンパイルしたあとデバッグ情報を取り出して手動で保存した僕と一緒にコードを書きましょう。

ちなみに先ほどの文で呆れて読み飛ばした人には内緒ですがなぜか外部にもシェーダーバイナリにもデバッグ情報を置いておかないとPIXでのデバッグができなかったです。たぶん他のやり方はありますがプリパラ見てたら考えるのがめんどくさくなりました。

  • -Zss

コンパイル情報に(ソースファイルとコンパイルした後のバイナリ)から生成したハッシュを含めます。実はPixでのデバッグに重要になります。

コマンドライン引数を構築するヘルパー関数

これいる?と思いますがまあ。。。うーん。。。別にLPCWSTRの動的配列とかで引数を追加していってもいいし、動的確保がいやなら静的な配列を使ってもいいと思います。まああるので使ってもいいかな、と。特に説明はしませんが使い方はほぼ動的配列みたいなものなのでわかると思います。どうせ実装も動的配列だと踏んでいます。 これ

Include Handler

         ComPtr<IDxcIncludeHandler> defaultInc;
            hr = m_pUtils->CreateDefaultIncludeHandler(defaultInc.ReleaseAndGetAddressOf());
            if (FAILED(hr)) {
                assert(false);
                return false;
            }

これでデフォルトのIncludeHandlerを取得できます。これはつまりファイルシステムからインクルードするということらしいです。

カスタムのinclude handlerはあまり調べてないですがIDxcIncludeHandlerを継承して作ればいいらしいです。たぶん作らなくてもいいと思います。

コンパイル結果から情報を取得

         hr = m_pCompiler->Compile(
                &srcBuff,
                pCmpArgs->GetArguments(),
                pCmpArgs->GetCount(),
                defaultInc.Get(),
                IID_PPV_ARGS(compiledBuff.ReleaseAndGetAddressOf())
            );

コンパイルするとcompiledBuffと書いているIDxcResult型の変数に結果が入れられますが、さらにこのResultからIDxcBlobをとってこないといけません。

エラー処理

HRESULT型見れば成功かエラーかくらいわかるでしょ、って感じなんですがS_OKでもエラーになってたりします。その場合はエラー情報がIDxcResultから取得できるので、HRESULT型にキレながら実装しましょう。

         ComPtr<IDxcBlobUtf16> outputName{};
            ComPtr<IDxcBlob> errBlob{};
            hr = compiledBuff->GetOutput(DXC_OUT_ERRORS, 
                IID_PPV_ARGS(errBlob.ReleaseAndGetAddressOf()),
                outputName.ReleaseAndGetAddressOf());

IDxcResult型から情報を取得するにはGetOutputを使います。DXC_OUT_ERRORSの部分でどの情報を取得するか決めます。これでerrBlobに中身が入っていたら絶望しながらエラー処理とメッセージの出力をお願いします。

コンパイルした情報を全部取得

ほい

         ComPtr<IDxcBlob> compiledShaderBlob{};
            hr = compiledBuff->GetResult(compiledShaderBlob.ReleaseAndGetAddressOf());
            if (FAILED(hr)) {
                assert(false);
                return false;
            }

コンパイルした情報からいろいろ取得してみる。

pdbファイルを保存

要するにデバッグ用情報ということです。これがないとPIXでシェーダーデバッグしよっかな~、とお散歩に出たら目の前でアセンブラが仁王立ちしている景色に出会います。全裸のアセンブラはさすがに目に毒なのでデバッグ情報を保存しておいてPIXではちゃんと服を着たシェーダーコードを読みましょう。

         hr = m_pContainerRefl->Load(compiledShaderBlob.Get());
            //pdb名を取得
            UINT32 debugNameIndex;
            hr = m_pContainerRefl->FindFirstPartKind(hlsl::DFCC_ShaderDebugName, &debugNameIndex);
            ComPtr<IDxcBlob> pPdbName;
            hr = m_pContainerRefl->GetPartContent(debugNameIndex, pPdbName.ReleaseAndGetAddressOf());
            //デバッグ情報の取得
            UINT32 debugInfoIndex;
            hr = m_pContainerRefl->FindFirstPartKind(hlsl::DFCC_ShaderDebugInfoDXIL, &debugInfoIndex);
            ComPtr<IDxcBlob> pPdb;
            hr = m_pContainerRefl->GetPartContent(debugInfoIndex, pPdb.ReleaseAndGetAddressOf());
            //pdbファイル名の変換
            auto pDebugNameData = reinterpret_cast<hlsl::DxilShaderDebugName const*>(
                pPdbName->GetBufferPointer());
            auto pName = reinterpret_cast<char const*>(pDebugNameData + 1);

基本的にIDxcContainerReflectionにバイナリ情報を読ませて、FindFirstPartKindで情報がどこにあるかを探す。GetPartContentで情報を取得。という流れです。

コードではpdbファイルの名前とその中身を取得しています。pdbファイルは後でPIXに読ませるのでファイルとして保存しておきましょう。

ファイル名を取得

ファイル名まで無効に決められると俺に指図するな!と言いたくなりますが、そうしたらPIXがへそを曲げてpdbファイルを自動で呼んでくれなくなるので権威には逆らわずに生きていきましょう。

取得方法はコードを読んでの通りですが、この名前は先のZss引数で生成されたハッシュ値となっています。

ファイルを保存してPIXにパスを設定

さっきの名前を使ってデバッグ情報をpdbファイルとして雑な場所に保存します。この場所をPIXに教えてあげます。PIXの左上のHomeを押してSettings>Symbol/PDB Options>PDBSearch Pathsです。そこに保存先のpdbがあるディレクトリを教えてあげるのです。

これでPIXが勝手にpdbファイルを読んでくれます。

問題点

pdbのファイル名がハッシュ値なので更新のしようがありません。ソースファイルを変えるたびに違ったハッシュ値が生成されるのでそのたびに違う名前のpdbファイルが増えていきます。古いpdbファイルを消したいんですけどどれが消すべきファイルなのかわからないんですよねー。

幸いただのデバッグ用ファイルなのでめちゃくちゃ多くなったら消すくらいでいいと思います。本気で参照されないpdbを消すようにするなら以下の二通りでしょうか。

  • シェーダーファイル名とpdbファイル名のマップをファイルに保存しておく。
  • 毎回pdbフォルダを全部消して、全シェーダーファイルのpdbを毎回生成する。

まあ使わないpdbがあってもいいんですけど、公式のドキュメントにはそれとは別に「いっぱいpdbがあったらさがすのに時間かかっちゃうよ~;;!ふぇぇぇぇ~」と書いています。公式ドキュメント幼女。そういう場合はpdbファイルをzipにまとめてしまうのがいいとか。PIXが気合でzipから読みだすらしいです。

あとpdbファイルを保存した後にデバッグ情報をバイナリから削除することができるのですが、

         //pdbを除いた情報を生成
            ComPtr<IDxcContainerBuilder> pDxcContainerBuilder;
            ::DxcCreateInstance(CLSID_DxcContainerBuilder,
                IID_PPV_ARGS(pDxcContainerBuilder.ReleaseAndGetAddressOf()));
            pDxcContainerBuilder->Load(compiledShaderBlob.Get());
#if defined(NDEBUG)
            pDxcContainerBuilder->RemovePart(hlsl::DFCC_ShaderDebugInfoDXIL);
#endif // !defined(_DEBUG)|| defined(DEBUG)
            ComPtr<IDxcOperationResult> pstrippedResult;
            pDxcContainerBuilder->SerializeContainer(pstrippedResult.ReleaseAndGetAddressOf());
            pstrippedResult->GetResult((IDxcBlob**)&pcompiledShaderObj);

これを行うとなんかシェーダーのステップ実行ができなくなります。なんででしょうね。どちらかにデバッグ情報があれば十分なはずなんですけどね。そういうわけでShaderDebugInfoを消す部分はRelease版だけ行うことにしています。そもそもRelease版だとコンパイルの時点からデバッグ情報を持たせなければいいのですけど。

これでPIXでのデバッグができます。世の中の内製開発環境や大してサポートされていないような環境では割とシェーダーのステップ実行ができなかったり、シェーダーコードがアセンブラになってしまう症状は起きうるのですが、これを見るにpdbファイルかシェーダーバイナリ内のデバッグ情報が欠けているということですね。なるほどです。

シェーダーリフレクションを取得

     //shader reflectionの取得
            auto hr = m_pContainerRefl->Load(pcompiledShaderObj);


            UINT shdIdx = 0;
            hr = m_pContainerRefl->FindFirstPartKind(DXC_PART_DXIL, &shdIdx);
            hr = m_pContainerRefl->GetPartReflection(shdIdx,
                IID_PPV_ARGS(_ppRflc));

このコードではID3D12ShaderReflection** _ppRflcにシェーダーリフレクションを取得しています。シェーダーリフレクションはシェーダーのソースバインド情報とかが取得できます。僕のライブラリだとシェーダーリフレクション情報をもとにID3D12Resourceとかそのあたりを自動生成しているのでこれがなくなると終わりです。

ちなみにシェーダーリフレクション情報もシェーダーバイナリから削除することができます。もったいないぃぃ。

おしまい

誰もdxcompiler.libとかc++からコンパイルしていなかったのでよっぽど需要がないのだな、と思い誰からも読まれる心配がないので雑に書くことができました。個人的にはすでにD3DCompileFromFile関数を使ってシェーダーコンパイラークラスを実装していたのでその代替処理としてdxcでもc++からコンパイルしたかったのが動機です。

あとECSがどうこうと言っていましたが、どうせECSは仕事やってるうちに勉強する機会がありそうだったのでMeshShaderでの描画についてやっていきます。