LoginSignup
11
7

More than 5 years have passed since last update.

[SPIR-V]シェーダの特殊化定数を使ってみる

Posted at

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はマップエントリの配列の先頭要素へのポインタ、dataSizepDataのサイズ(バイト単位)で、pDataが定数のマッピングで使われるデータになります。
VkSpecializationMapEntry構造体はさらに以下のようになっています。

struct VkSpecializationMapEntry
{
    uint32_t constantID;
    uint32_t offset;
    size_t size;
}

constantIDでシェーダ側のconstant_idを指定し、VkSpecializationInfopDataoffsetバイトから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という値を割り当てることができます。

11
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
7