注意書き

本サイトでは、アフィリエイト広告およびGoogleアドセンスを利用しています。

Vulkan: シェーダーメイン関数を指定する

プログラミング

Vulkan を使用しており、シェーダーソースコードを作成しているとき、メイン関数(エントリーポイント)をちょっと変えたいことがあると思います。例えば、共通コードが多めで、メイン関数からの呼び出し部が少しだけ変化するような場合です。

スポンサーリンク

前提や背景など

共通コードは別ファイルに逃がして、 #include によって再利用するという方法があります。しかし、GLSLの世界では、拡張機能を使う必要があり、避けたい気持ちになります。

共通コードという部分は、ユニフォームバッファーのレイアウト定義や、共通関数を指しています。他にも、定数が含まれます。これらを、複数ファイルで重複した記述をしたくないため、1つのソースコードで複数のメイン関数を記載して呼び分けるようなことをしたいと考えました。

厳密に同じではありませんが近いイメージとしては、昔 DirectX 使用場面で使われていた *.fx ファイルに近いものでしょうか。あれは頂点シェーダーとピクセルシェーダーで別々のエントリポイントを指定して1ソースで管理ができました。

案1:プリプロセッサ定義で分離

メイン関数を分けるという点で一番簡単なのは、 プリプロセッサ定義を使用して分けるという方法です。例えば以下のように、コード内でのメイン関数(エントリポイント)は ifdef で囲っておき、コンパイル時に、プリプロセッサ定義を設定して、1つのエントリポイントを選択するという形です。

void doAction(int a, int b, int c)
{
   ...
}
void doPostAction() { ... }

#ifdef USE_MAIN_TEST1
void main()
{
  doAction(1, 2, 3);
}
#endif

#ifdef USE_MAIN_TEST2
void main()
{
  doAction(4, 5, 6);
  doPostAction();
}
#endif

このとき、コンパイルするときのコマンド例は次のようになります。”-D”オプションにて有効とする部分の定義を行います。

glslangValidator -V --target-env vulkan1.1 -S comp mycode.comp -DUSE_MAIN_TEST1  -o output.spv

案2: 別々のエントリポイント名にする

本記事の目玉です。シェーダーコード内にエントリポイントとなる関数を複数記載したままにします。先ほどのコード例と同様なものを挙げると、以下のようになります。test1やtest2として、実装を行います。

void doAction(int a, int b, int c)
{
   ...
}
void doPostAction() { ... }

void test1()
{
  doAction(1, 2, 3);
}

void test2()
{
  doAction(4, 5, 6);
  doPostAction();
}

このコードをコンパイルするときに、どの関数をエントリポイントとするかを設定します。これには次のようなパラメータを与えます。

glslangValidator -V --target-env vulkan1.1 -S comp mycode.comp  -e test1 --source-entrypoint main  -o output.spv

ここで、”-e test1 –source-entrypoint main” の部分がポイントでした。この2つのパラメータの組み合わせでエントリポイントがない、というエラーを避けることができました。

このようにコンパイルしたシェーダーコードを使うときにも注意が必要です。VkPipelineShaderStageCreateInfoの .pName に使用するエントリポイント名の記載が必要でした。多くの場面で “main” となっているかと思いますが、シェーダーコンパイル時の -e オプションで設定したものと合致したものにします。

VkPipelineShaderStageCreateInfo shaderStageCS{
  .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
  .pNext = nullptr,
  .flags = 0,
  .stage = VK_SHADER_STAGE_COMPUTE_BIT,
  .module = LoadShaderModule("output.spv"),
  .pName = "test1",  // シェーダーコード内のエントリポイントに指定した関数名
  .pSpecializationInfo = nullptr,
};

まとめ

これらの2案でシェーダーソースコードの実装で、なるべく複数ファイルを作らないという方法を実現できそうです。ただし *.fx とは違う使い勝手です。*.fxに近いことをしようとすると ifdef で分けるしかなさそうです。また案2についても、実装が違うバリエーションがある場合に使えると思いますが、ディスクリプタセットが違ったり、定義するスレッドサイズの違い(local_size) があったりすると、もう使えません。

こういうことがやれたらいいのに、と思っている人に向けて本記事が1つのヒントになれば幸いです。

コメント

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