シェーダーコードのリフレクションを皆さんは使用していますか?
DirectXで長らく使われてきた D3DCompiler (fxc) を用いて作られたシェーダーバイナリについては、 D3DReflect 関数を用いてシェーダーリフレクションにアクセス出来ます。
近年は徐々に fxc から dxc へ、DirectX Shader Compiler を使用する場面も増えてきているように思います。ようやく dxc でコンパイルされたシェーダーバイナリについて、シェーダーリフレクションが必要になる場面も出てくるかなと思います。
しかし、なかなか情報がなさそうなので今回の記事を作成するに至りました。今回以下の画面はシェーダーリフレクションによって、dxc でコンパイルされたシェーダーの結果を表示したものです。シェーダーは拙作同人誌の DirectX12 Programming Vol.2 のキャラクター描画用のものです。
D3DReflect の場合
先に fxc でコンパイルされたシェーダーバイナリについてみておきます。こちらは簡単で、シェーダーバイナリファイルを読み込んで、D3DReflect 関数に入力すると、ID3D12ShaderReflectionインターフェースを取得できます。
ifstream infile(fileName, ios::binary); // シェーダー(バイナリ)を読み込む
vector<char> data;
data.resize(infile.seekg(0, ios::end).tellg());
infile.seekg(0, ios::beg).read(data.data(), data.size());
ComPtr<ID3D12ShaderReflection> shaderReflection;
D3DReflect(data.data(), data.size(), IID_PPV_ARGS(&shaderReflection));
このインターフェースからは、D3D12_SHADER_DESC構造体の情報を得ることが出来ます。この構造体の定義は以下となっており、多くの情報が詰まっています。各種項目ごとに個数が分かるので、そこから他のインターフェースを取得して逐次パラメータの情報を集めていく形で、シェーダーの情報が得られます。この部分は DirectXShaderCompiler の場合でも同じ処理となるので、後ほどコード例を紹介します。
typedef struct _D3D12_SHADER_DESC
{
UINT Version; // Shader version
LPCSTR Creator; // Creator string
UINT Flags; // Shader compilation/parse flags
UINT ConstantBuffers; // Number of constant buffers
UINT BoundResources; // Number of bound resources
UINT InputParameters; // Number of parameters in the input signature
UINT OutputParameters; // Number of parameters in the output signature
UINT InstructionCount; // Number of emitted instructions
UINT TempRegisterCount; // Number of temporary registers used
UINT TempArrayCount; // Number of temporary arrays used
UINT DefCount; // Number of constant defines
UINT DclCount; // Number of declarations (input + output)
UINT TextureNormalInstructions; // Number of non-categorized texture instructions
UINT TextureLoadInstructions; // Number of texture load instructions
UINT TextureCompInstructions; // Number of texture comparison instructions
UINT TextureBiasInstructions; // Number of texture bias instructions
UINT TextureGradientInstructions; // Number of texture gradient instructions
UINT FloatInstructionCount; // Number of floating point arithmetic instructions used
UINT IntInstructionCount; // Number of signed integer arithmetic instructions used
UINT UintInstructionCount; // Number of unsigned integer arithmetic instructions used
UINT StaticFlowControlCount; // Number of static flow control instructions used
UINT DynamicFlowControlCount; // Number of dynamic flow control instructions used
UINT MacroInstructionCount; // Number of macro instructions used
UINT ArrayInstructionCount; // Number of array instructions used
UINT CutInstructionCount; // Number of cut instructions used
UINT EmitInstructionCount; // Number of emit instructions used
D3D_PRIMITIVE_TOPOLOGY GSOutputTopology; // Geometry shader output topology
UINT GSMaxOutputVertexCount; // Geometry shader maximum output vertex count
D3D_PRIMITIVE InputPrimitive; // GS/HS input primitive
UINT PatchConstantParameters; // Number of parameters in the patch constant signature
UINT cGSInstanceCount; // Number of Geometry shader instances
UINT cControlPoints; // Number of control points in the HS->DS stage
D3D_TESSELLATOR_OUTPUT_PRIMITIVE HSOutputPrimitive; // Primitive output by the tessellator
D3D_TESSELLATOR_PARTITIONING HSPartitioning; // Partitioning mode of the tessellator
D3D_TESSELLATOR_DOMAIN TessellatorDomain; // Domain of the tessellator (quad, tri, isoline)
// instruction counts
UINT cBarrierInstructions; // Number of barrier instructions in a compute shader
UINT cInterlockedInstructions; // Number of interlocked instructions
UINT cTextureStoreInstructions; // Number of texture writes
} D3D12_SHADER_DESC;
DirectXShaderCompiler の場合
dxc でコンパイルされたシェーダーバイナリでは、 先に紹介した D3DReflect 関数が使用できません。シェーダーバイナリのヘッダこそは一致していますが、含まれている内容が異なるためだと考えられます。 dxc の場合にはシェーダーバイナリの中に記録されているのが DXIL コードとなっているためです。fxc でコンパイルされた結果も中間コードとなっており、DXILもまた中間コードですが、両者は同一ではありません。ただドライバの努力によってどちらも動作出来るようになっているようです。
Dxc API よりリフレクション情報を得る
この世代のシェーダーコードからシェーダーリフレクション情報を得るためには、dxc API を用います。IDxcContainerReflection インターフェースを用いて、DXILコードが記録されたブロックを特定し、そのブロックから先に紹介した ID3D12ShaderReflectionインターフェースを取得します。
ここまでの情報取得コードの例は以下の通りです。いくつかのコードは省略しているので少しご注意ください。
#include <dxcapi.h>
#pragma comment(lib, "dxcompiler")
...
// dxc api を使う.
ComPtr<IDxcLibrary> lib;
DxcCreateInstance(CLSID_DxcLibrary, IID_PPV_ARGS(&lib));
ComPtr<IDxcBlobEncoding> binBlob{};
lib->CreateBlobWithEncodingOnHeapCopy(data.data(), data.size(), CP_ACP, &binBlob);
ComPtr<IDxcContainerReflection> refl;
DxcCreateInstance(CLSID_DxcContainerReflection, IID_PPV_ARGS(&refl));
// シェーダーバイナリデータをロードし、DXILチャンクブロック(のインデックス)を得る.
UINT shdIndex = 0;
refl->Load(binBlob.Get());
refl->FindFirstPartKind(DXIL_FOURCC('D', 'X', 'I', 'L'), &shdIndex);
// シェーダーリフレクションインターフェース取得.
ComPtr<ID3D12ShaderReflection> shaderReflection;
refl->GetPartReflection(shdIndex, IID_PPV_ARGS(&shaderReflection));
定数バッファの情報
得られたインターフェースから定数バッファの情報を抜き出す部分を紹介します。D3D12_SHADER_DESC構造体にバッファ数が記録されるのでそこから、 GetConstantBufferByIndex メソッドにより D3D12_SHADER_BUFFER_DESC 構造体を得ます。これはバッファの情報のみになっており、バッファの中にどのような変数が入っているのかを知るために、ID3D12ShaderReflectionVariable, ID3D12ShaderReflectionTypeらのインターフェースを取得してさらなる情報を集めています。
void Display(ComPtr<ID3D12ShaderReflection> refl)
{
stringstream ss;
D3D12_SHADER_DESC desc{};
refl->GetDesc(&desc);
ss << setw(40) << setfill('-') << left;
ss << "ConstantBuffers " << setfill(' ') << endl;
const auto cbCount = desc.ConstantBuffers;
for (auto i = 0; i < cbCount; ++i)
{
D3D12_SHADER_BUFFER_DESC shaderBufDesc{};
auto cbuffer = refl->GetConstantBufferByIndex(i);
cbuffer->GetDesc(&shaderBufDesc);
ss << left;
ss << " " << shaderBufDesc.Name << endl;
for (auto j = 0; j < shaderBufDesc.Variables; ++j)
{
D3D12_SHADER_VARIABLE_DESC varDesc{};
D3D12_SHADER_TYPE_DESC typeDesc;
ComPtr<ID3D12ShaderReflectionVariable> varRefl = cbuffer->GetVariableByIndex(j);
ComPtr<ID3D12ShaderReflectionType> varTypeRefl = varRefl->GetType();
varRefl->GetDesc(&varDesc);
varTypeRefl->GetDesc(&typeDesc);
ss << " " << setw(20) << std::left << varDesc.Name;
ss << setw(10) << typeDesc.Name;
ss << "Size:" << setw(6) << std::right << varDesc.Size << endl;
}
}
入出力情報の取得
シェーダー関数の入出力についてもリフレクションで取得できます。 GetInputParameterDesc, GetOutputParameterDesc メソッドにより取得できる D3D12_SIGNATURE_PARAMETER_DESC 構造体によって示されます。
ss << "InputSemantics " << setfill(' ') << endl;
const auto iaCount = desc.InputParameters;
for (auto i = 0; i < iaCount; ++i)
{
D3D12_SIGNATURE_PARAMETER_DESC paramDesc{};
refl->GetInputParameterDesc(i, ¶mDesc);
ss << paramDesc;
}
ss << endl;
ss << setw(40) << setfill('-') << left;
ss << "OutputSemantics " << setfill(' ') << endl;
const auto outCount = desc.OutputParameters;
for (auto i = 0; i < outCount; ++i)
{
D3D12_SIGNATURE_PARAMETER_DESC paramDesc{};
refl->GetOutputParameterDesc(i, ¶mDesc);
ss << paramDesc;
}
この構造体の情報を出力するコードの例は以下となります。ここ必要な一部分のみ出力しています。構造体にはもっと多くの情報が入っているので、一度確認してみることをお勧めします。
入力のコンポーネント数を計算する部分は、ビット組み合わせによるものらしいですが、定義が見つからなかったので自前で求めています。この計算が合っているかの保障はないですが手元の範囲ではうまくいっているようにみえます。
ostream& operator<<(ostream& os, const D3D12_SIGNATURE_PARAMETER_DESC& desc)
{
std::string semantics(desc.SemanticName);
semantics += to_string(desc.SemanticIndex);
os << " " << setw(16) << semantics;
os << " ";
switch (desc.ComponentType)
{
case D3D_REGISTER_COMPONENT_UINT32:
os << "uint"; break;
case D3D_REGISTER_COMPONENT_SINT32:
os << "sint"; break;
case D3D_REGISTER_COMPONENT_FLOAT32:
os << "float"; break;
default:
os << "Unknown";
break;
}
BYTE elementCount = desc.Mask;
elementCount = ((elementCount & 0xA) >> 1) + ((elementCount & 0x5));
elementCount = ((elementCount & 0xC) >> 2) + ((elementCount & 0x3));
if (elementCount > 1)
{
os << uint32_t(elementCount);
}
os << endl;
return os;
}
このように実装して出力したものが冒頭で紹介した画像となります。
ID3D12LibraryReflection の罠
さて多くの場合はここまでのリフレクションで事足りるのですが、 最近のシェーダーモデルで追加された lib プロファイルのものは、情報を得ることが出来ません。これには ID3D12LibraryReflection インターフェースでアクセスすることになりますが、この使用過程でしらないとやっかいな点がありました。
このやっかいな点とは以下の点です。
- lib プロファイルでコンパイルされていない DXIL ブロックを渡しても
正常終了として ID3D12LibraryReflection インターフェースが取得できてしまう - 得られたインターフェースで GetDesc メソッドで情報取得を行うと、メモリ破壊を引き起こす。このときのメソッド実行結果も正常終了を返す
この問題があるので、1つの安全な実装例として以下のコードに落ち着きました。これでメモリ破壊はしなくなります。
// 通常のシェーダーでも ID3D12LibraryReflection が取得できてしまう.
// またそれを用いて、D3D12_LIBRARY_DESC を取得を試みると正常完了し、メモリを破壊する動きがある.
// このプログラムではそれを union を用いて防止している.
union {
D3D12_SHADER_DESC shaderDesc;
D3D12_LIBRARY_DESC libDesc;
} ReflectionDesc;
auto hr = libReflection->GetDesc(&ReflectionDesc.libDesc);
まとめ
シェーダーリフレクションについて今回紹介しました。D3DRelfect関数を使った方式は多くの箇所で紹介されており、しかも扱いやすいので使ったことがある人もいるでしょう。でも dxc のほうは、なかなか情報が少なく変なところで手間取ってしまいました。
拙作の DirectX12 Programming シリーズでは シェーダーコンパイラに fxc ではなく、 dxc を使う方を推しているのでリフレクションについても今回の方式を紹介して、普及推進したいと思います。