DXC/HLSLで複数のエントリーポイントを単一バイナリに事前コンパイル

## はじめに

DirectX/HLSLでアプリケーションを作る際、シェーダーのコンパイル・読み込み方法には主に2つの方法があります。

- 事前にFXC/DXCを使ってバイナリファイルにコンパイルしておいて、実行時に読み込む。
- 実行時にシェーダーソースからコンパイルを行う。

事前コンパイルのほうがシェーダーの正当性を予めチェックできるし、実行時に行うことも減るので好ましいと思うのですが、事前コンパイル時にはエントリーポイントの指定が必要で、少なくともVisual Studioの基本機能で完結するには基本的に１シェーダーファイル１エントリーポイントになってしまいます。
これではなにかの実験中に新たなシェーダーを追加したいと思ったときも面倒な操作(ファイル作成、ファイル名考案、コンパイル設定...)が一々必要です。共通のコードをincludeするだけの複数のシェーダーファイルを作ってエントリーポイント設定を変えるようにすれば書く対象のファイルは1つに絞れるものの他の手間はあまり変わりません。さらにこれらの方法でもバイナリファイルは結局複数個生成されてしまうのでランタイムにおける管理が煩雑になる問題は解決されていません。

ある程度の規模のゲームエンジンならシェーダーファイル生成が自動化されていて気にするところではないのかもしれませんが、小さなサンプルプログラムを作る程度だと話は変わりそうです。

DirectX Ray Tracing (DXR)では、端からlibプロファイルを使って複数のエントリーポイントを単一バイナリにコンパイルすることができます。要はこのlibプロファイルを非レイトレのシェーダーでも使う方法が無いのか調べて実験したところ、**DXRとは異なる手順でしたが可能であることがわかりました。**

## 実装

以下のような複数のエントリーポイントを持ったシェーダーファイルがあるとします。

```hlsl
// ...

[shader("vertex")]
Varying vs_main(const Attribute attr) {
    // ...
}

[shader("pixel")]
float4 ps_main(const Varying var) : SV_Target0 {
    // ...
}
```

DXRのシェーダーのように、各エントリーポイントに `[shader(...)]` というシェーダー種別を識別する属性を付加する必要があります。このシェーダーを `-T "lib_6_5"` のようにライブラリプロファイルで事前コンパイルします。おそらくシェーダーモデル6.3導入時にDXCに改めて追加された機能なので、DXCかつ6.3以降の指定が必要です。

続いてホストコード側において、シェーダーバイナリからシェーダーBlobを生成する手順を示します。(ごちゃごちゃしないようにエラー処理などを省いています。)

```cpp

#include <wrl.h>
#include <dxcapi.h>

template <typename T>
using Ref = Microsoft::WRL::ComPtr<T>;

// ...

Ref<IDxcUtils> utils;
DxcCreateInstance(CLSID_DxcUtils, IID_PPV_ARGS(&utils));

Ref<IDxcLinker> linker;
DxcCreateInstance(CLSID_DxcLinker, IID_PPV_ARGS(&linker));

std::vector<const wchar_t*> libNames = {
    L"library_name0"
    // ...
};

// 事前コンパイルで生成したバイナリからライブラリのBlobを作成する。
const std::vector<char> lib0Bin = readBinaryFile(lib0BinPath);
Ref<IDxcBlobEncoding> lib0Blob;
utils->CreateBlob(
    lib0Bin.data(), static_cast<uint32_t>(lib0Bin.size()),
    CP_ACP, &lib0Blob);

// ライブラリBlobを名前とともにリンカーに登録する。
linker->RegisterLibrary(libNames[0], lib0Blob.Get());
//linker->RegisterLibrary(libNames[1], lib1Blob.Get());
// ...

// 必要あればリンク時オプションを指定する。
std::vector<const wchar_t*> linkArgs = {
    // ...
};

// エントリーポイントとプロファイル、関連するライブラリ名を指定して
// リンク処理を行いシェーダーを生成する。
Ref<IDxcOperationResult> linkResult;
linker->Link(
    L"vs_main", L"vs_6_5",
    libNames.data(), static_cast<uint32_t>(libNames.size()),
    linkArgs.data(), static_cast<uint32_t>(linkArgs.size()),
    &linkResult);

HRESULT linkStatus;
linkResult->GetStatus(&linkStatus);

// リンクが成功していればシェーダーのBlobを取り出す。
Ref<IDxcBlob> vsBlob;
if (SUCCEEDED(linkStatus)) {
    linkResult->GetResult(&vsBlob);
}
else {
    IDxcBlobEncoding* linkerErrorMsg;
    linkResult->GetErrorBuffer(&linkerErrorMsg);
    printf("%s\n", reinterpret_cast<const char*>(linkerErrorMsg->GetBufferPointer()));
}
```

例としてVertex Shaderだけを生成していますが、Pixel ShaderのBlobも同様に生成できます。あとはPSO生成時に以下のようにしてバイナリを指定すれば完了です。

```cpp
D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc = {};
// ...
psoDesc.VS = { vsBlob->GetBufferPointer(), vsBlob->GetBufferSize() };
psoDesc.PS = { psBlob->GetBufferPointer(), psBlob->GetBufferSize() };
// ...
```

## 注意点
- libプロファイルを使う関係上DirectX Compiler (DXC)かつシェーダーモデル6.3以降に限定されます。
- リソースバインディング先となる変数の定義、レジスター設定に注意が必要。
  - HLSLの文法上、これらの変数はグローバル空間に定義されるため、一切関係の無いシェーダー間でも変数名衝突に気を遣う必要があります。
  - 変数名を分けていたとしても、無関係なシェーダー間でレジスターがオーバーラップしているとエラーが出ます。(レジスタースペースを分ければ一応はエラーは回避できますが)
  - 上記の点より、同一ファイルに書くシェーダーは同じルートシグネチャーを共有するものにすべきかもしれません。

## 参考
- [Linking DXIL Binaries Using DXC](https://therealmjp.github.io/posts/dxil-linking/)
ここでは「単一ファイル、複数エントリーポイント」ではなく「複数ファイル、単一エントリーポイント」の例が示されている。関数定義を別でコンパイルしておく場合など。
- [Shader Model 6.3](https://github.com/microsoft/DirectXShaderCompiler/wiki/Shader-Model-6.3)
"It also includes official HLSL compiled DXIL libraries and linking of shaders from these libraries, which has applications beyond raytracing."
とあるので、レイトレ以外のシェーダーでもライブラリプロファイルを使うことは想定されているようです。