Vulkanにおいて、シェーダ周りのインフラの一つとして新たに「SPIR-V」が採用されました。SPIR(Standard Portable Intermediate Representation)1-VはLLVMをベースとした、シェーダの新しい、そしてオープンな中間表現です(もともとはOpenCL向けのシェーダ中間表現として策定されたらしい)。
このSPIR-Vですが、Vulkanと合わさることにより従来のシェーダ中間表現(DirectXの内部で使われていたようなもの)と一つ違う特徴を持つようになりまして、それが今回の**特殊化定数(Specialization Constants)**となります。
特殊化定数について軽く
特殊化定数とはシェーダのプログラム中に定数を実行時に埋め込める機能です。SPIR-Vは単なる中間表現なので実際にVulkanドライバによってGPUネイティブに翻訳される前にいくらか処理を施すことが可能で、それを活用したものとなります。
特殊化定数を使う利点
特殊化定数を使わず通常のuniformバッファで値を受け渡した場合、その値を読み出すためにシェーダはメモリロード命令(SPIR-V内部ではOpLoad
)を使います。一方で特殊化定数を使った場合、シェーダ内部でメモリロードは発生しないようになります(SPIR-V内部では命令にOpSpecConstant
のラベルがそのまま入る)。今の時代でシェーダのメモリロード1命令が「何かパフォーマンスに重大な影響を及ぼす」だとか「あと1命令削らないと入らない」だとかということはあまりない気がしますが(もちろん場合による)(格納可能な命令数でいえばSM4.0で制限がなくなったようなので今ではやりくりする必要はあまりない気がします)。
欠点
パイプライン作成時に値を固定してしまうためコマンドリストの内部で変更するなどといったことはできません。場合によってPush Constantsを使う必要があります。また特殊化できるものは定数に限るため、例えばテクスチャなどを特殊化することはできません(あとよくわかりませんが#version 450
で使っているとvec3なども特殊化できません)。DescriptorSetとPush ConstantsとSpecialization Constantsをうまく使い分けましょう。
実際に使ってみる
使ってみるというか、制作中のゲームから使用事例を紹介する感じになります。
弾幕シューティングなのですが、自機の弾のスケーリングに特殊化定数を使っています。
詳しいことはPctg-x8/hardgrad_extentを見ていただくとして、自機の弾を描画するパイプラインの頂点シェーダ内部で、以下のように特殊化定数を宣言しています。
layout(constant_id = 0) const float SpriteScaling = 1.0f;
あくまでもGLSLでの扱いは定数なので初期値の設定が必要です。layout
を使ってin/out変数のlocationを宣言するのと同様にconstant_id
で定数のID番号を宣言します。GLSL側ではこれで準備完了で、あとは普通に定数の一つとして扱うことができます。
次はこの定数をプログラム側から書き換えます。ゲームのほうではInterludeを通してるので詳細が隠れてしまっているので改めて分解して説明します(Interludeとはなんぞやというのは12/18まで待ってください)。
Vulkanで特殊化定数を特殊化する場合は、パイプライン作成時のシェーダステージの設定の中で設定を行います。
VkPipelineShaderStageCreateInfo
構造体の中にpSpecializationInfo
と名づけられたVkSpecializationInfo
構造体へのポインタを設定する部分があります。特殊化定数を使わない場合はnullptr
でかまわないのですが、使う場合は正当なポインタである必要があります。
このVkSpecializationInfo
構造体は以下のようになっており、意外と面倒柔軟な仕組みになっています。
struct VkSpecializationInfo
{
uint32_t mapEntryCount;
const VkSpecializationMapEntry* pMapEntries;
size_t dataSize;
const void* pData;
}
mapEntryCount
はマップエントリの数、pMapEntries
はマップエントリの配列の先頭要素へのポインタ、dataSize
はpData
のサイズ(バイト単位)で、pData
が定数のマッピングで使われるデータになります。
VkSpecializationMapEntry
構造体はさらに以下のようになっています。
struct VkSpecializationMapEntry
{
uint32_t constantID;
uint32_t offset;
size_t size;
}
constantID
でシェーダ側のconstant_id
を指定し、VkSpecializationInfo
のpData
のoffset
バイトからsize
バイトまでを特殊化定数の中身として割り当てます。実例として、
auto upData = std::make_unique<float[1]>({0.2f});
auto mapEntry = VkSpecializationMapEntry { 0, 0, sizeof(float) };
auto specInfo = VkSpecializationInfo { 1, &mapEntry, sizeof(float), upData.get() };
とすることで、特殊化定数のID0番に0.2f
という値を割り当てることができます。