DirectXShaderCompiler 使用時のシェーダーリフレクション

シェーダーコードのリフレクションを皆さんは使用していますか?
DirectXで長らく使われてきた D3DCompiler (fxc) を用いて作られたシェーダーバイナリについては、 D3DReflect 関数を用いてシェーダーリフレクションにアクセス出来ます。

近年は徐々に fxc から dxc へ、DirectX Shader Compiler を使用する場面も増えてきているように思います。ようやく dxc でコンパイルされたシェーダーバイナリについて、シェーダーリフレクションが必要になる場面も出てくるかなと思います。

しかし、なかなか情報がなさそうなので今回の記事を作成するに至りました。今回以下の画面はシェーダーリフレクションによって、dxc でコンパイルされたシェーダーの結果を表示したものです。シェーダーは拙作同人誌の DirectX12 Programming Vol.2 のキャラクター描画用のものです。

頂点シェーダーの結果
DirectX12 Programming Vol.2 - すらりんラボ - BOOTH
DirectX 12 のプログラミング入門本 の第2弾です。 前回の “DirectX12 Programming Vol.1” では、ポリゴンの描画までを主対象とし、簡単なモデル形状を描画するまでを説明しました。 今回は、HDR10 の出力方法や、テクスチャレンダリングの実装方法、MikuMikuDance (MMD...

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もまた中間コードですが、両者は同一ではありません。ただドライバの努力によってどちらも動作出来るようになっているようです。

DirectX12 への統一理論 - すらりんラボ - BOOTH
DirectX12 の一般的にはなかなか知られていない側面について記述した同人誌です。実際に使える場面があるかどうか、役立つケースがあるかどうかも不明ですが、1つの読み物として DirectX12 に興味のある人はいかがでしょうか? ※ 本書は 2020/06/27 に開催予定だった 「第3回 技術書同人誌博覧会」に向...

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, &paramDesc);
    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, &paramDesc);
    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 メソッドで情報取得を行うと、メモリ破壊を引き起こす。このときのメソッド実行結果も正常終了を返す

メモリ破壊は自分のコードが悪いと疑って調査して、さんざん探し回り、この結果になりました。この挙動を見たのは、 Windows10 2004 バージョンです。Windows SDK としては 18362 や 19041 を切り替えてみたりしましたが、どちらも変わらずでした。もしかすると自分の環境問題の可能性も残っているのかもしれません。

この問題があるので、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 を使う方を推しているのでリフレクションについても今回の方式を紹介して、普及推進したいと思います。

DirectX12 Programming Vol.3 - すらりんラボ - BOOTH
Windows 10 で使える DirectX 12 のプログラミング入門本 の第3弾です。前回までの Vol.1 , Vol.2 で DirectX12 での描画の基本は出来るようになりました。 今回はグラフィックスのシェーダーステージの完結編となります。ジオメトリシェーダーやハルシェーダー、ドメインシェーダーを解説...
DirectX12 Programming Vol.2 - すらりんラボ - BOOTH
DirectX 12 のプログラミング入門本 の第2弾です。 前回の “DirectX12 Programming Vol.1” では、ポリゴンの描画までを主対象とし、簡単なモデル形状を描画するまでを説明しました。 今回は、HDR10 の出力方法や、テクスチャレンダリングの実装方法、MikuMikuDance (MMD...
DirectX12 Programming Vol.1 - すらりんラボ - BOOTH
Windows 10 で使える DirectX 12 のプログラミング入門本です。 DirectX12が使えるようになってからそれなりに時間が経過しましたが、書籍としてまとまっているものはまだ数が少ないかと思います。 Vol.1 では DirectX 12 の初期化から簡単な描画までをじっくりと解説しました。 低レイヤ...
DirectX
すらりんをフォローする
すらりん日記

コメント

タイトルとURLをコピーしました