HLSLからSPIR-Vバイナリを生成する

はじめに

今回は,Vulkan向け(OpenGLでも4.6なら対応:https://www.khronos.org/news/press/khronos-releases-opengl-4.6-with-spir-v-support)の話ですが,今回はHLSLからSPIR-Vバイナリを生成するについて少しまとめておきます.
VulkanではOpenGL 4.6以前と異なりシェーダはSPIR-Vを使用して中間コードを生成してそれを読み込んで実行されます.中間コードはあくまでも中間コードなので,実行はGPUごとにそデバイスごとのコードになっていきます.
OpenGLではシェーダコードはGLSLで書いていましたが,Vulkanでも基本はそうなのですがSPIR-Vの仕組みが入ったことで,フロントエンドの言語はなんでもよくて中間コード部分が重要になりました.
OpenGLでも長年のHLSLのコード資産があるようなところやマルチプラットフォームのHLSLを何とかしてGLSLに変換するなどを行っていたわけですが,VulkanではSPIR-Vのバイナリが作れてしまえばよくなるわけです.
あとは以下のような理由でHLSLが使いたいなどあるかもしれません.

  • #includeが使いたい
  • シェーダの中に大量のマクロを使用していてシェーダのコンパイル時に様々なバリエーションを生成するようなことをやっている

HLSLからSPIR-Vのバイナリを生成する方法については様々ありますが,一番基本的なツールそうなKhronosのglslangが使えると良いということで本記事ではこれを扱います.
KhronosGroup/glslang
https://github.com/KhronosGroup/glslang
他の方法についても一応,軽く触れておきます.

  • Shaderc
    • https://github.com/google/shaderc
    • Googleが提供している環境.glslangを使用しているので本家と互換性高い
    • 少し機能が違うが本家と近い
      • かつてはglslangでサポートしていなかったHLSLの#includeやエントリーポイントをmain以外に指定できるなどをサポートしていたが現在はglslangはサポート
  • HLSLcc
    • https://github.com/Unity-Technologies/HLSLcc
    • Unityの中でHLSLCrossCompilerをフォークして,HLSLをGLSLにする変換をしているクロスコンパイラ
      • Metalも対応
    • DXBC(HLSLのコンパイル後のバイナリコード)からGLSLに変換
      • OpenGLでも使える
      • 出来上がるGLSLは人間が読むには向かないです
      • バイナリコードからの変換なので#includeとかコンパイル時にマクロ設定などはfxc.exeかける時点で解決している
    • GLSLにできてしまえばSPIR-Vに流せる
  • DirectX Shader Compiler
    • https://github.com/Microsoft/DirectXShaderCompiler
    • MicrosoftのHLSL 6.x以降の新しいシェーダコンパイラ
    • SPIR-Vバイナリの出力をサポート
      • Googleの人の協力による
    • LLVMベース
    • HLSL 6.x以降のHLSLの変換はこっちでしかできない
      • 今回は扱いませんがHLSL 6.xメインで書くようになったら扱います

ビルド方法

依存する外部ライブラリとかあまりないのでCMakeがあればすんなりビルドする環境が構築できると思います.
VS 2017であれば,CMake機能が標準でありますのでそれで生成しても良いかと思います.
Visual Studio 2017のCMake対応機能の使い方
http://masafumi.cocolog-nifty.com/masafumis_diary/2017/03/visual-studio-2.html

glslangValidator.exeの使い方

Windows の場合,できあがったglslangValidator.exeを使用すればよいわけですが,HLSLの場合いくつかGLSLとコマンドラインの使い方などが異なります.

HLSLで使うときに知っておくこと

このツールでは,デフォルトのGLSLを使用する際にHLSLとは違ったルールがあるのですが,それについては下記にまとめられています.
https://github.com/KhronosGroup/glslang/wiki/HLSL-FAQ

HLSLをコンパイルするときの基本

glslangValidator.exeでHLSLをコンパイルするときには”-D”オプションを付けます.FAQを見るとシンプルなコンパイル例としては下記のような構文が提示されています.この例では,myshader.frragというHLSLのピクセルシェーダのファイルをtest.spvというSPIR-Vバイナリに出力しています(バイナリではなく人間が読めるILを出すオプションなどもあり).-eで指定しているのはシェーダのエントリポイントの関数名です.

glslangValidator -e main -o test.spv -V -D myshader.frag

拡張子

このツールはREADMe.mdにあるとおりファイルの拡張子でシェーダステージを判定しています.ただ,Visual StudioのHLSLのシンタックスハイライトなどを利用している方などは拡張子*.hlslを利用している方が多いかもしれません.これに関してはコマンドライン引数のオプションでコンパイルするシェーダステージを指定できるので問題ありません.

  • .vert for a vertex shader
    • 頂点シェーダ
  • .tesc for a tessellation control shader
    • Direct3DだとHull Shader
  • .tese for a tessellation evaluation shader
    • Direct3Dだとドメインシェーダ
  • .geom for a geometry shader
    • Direct3Dだとジオメトリシェーダ
  • .frag for a fragment shader
    • Direct3Dだとピクセルシェーダ
  • .compfor a compute shader
    • Direct3Dだとコンピュートシェーダ

オプションは”-S”でそのあとにシェーダステージを指定すればOKです.シェーダステージは上に書いた物から”.”(ドット)を取ったものです(頂点シェーダなら”vert”).

シェーダのエントリポイント

GLSLではシェーダのエントリポイントとなる関数をmain()にする方が多いためデフォルトではmain関数から呼ばれます.
拡張子のところを見るとわかりますが,1つのシェーダのファイルが1つのシェーダを表しているので1つのファイルに異なるシェーダの処理を書いてしまった場合ちょっと面倒なことになるわけですね.
この場合,オプション”-e”をつけてそのあとにエントリポイントとなる関数をつけます.

マクロ

コンパイル時にマクロを設定したいというのも対応しています.fxc.exeで言うと”/D”にあたります.
fxc.exeの構文
https://msdn.microsoft.com/ja-jp/library/bb509709(v=vs.85).aspx
ここで,ちょっと「おや」と思いますがすでにglslangValidatorでは”-D”は使用されています.ここでglslangValidator起動してオプションを見てみます.glslangValidatorにオプションをつけなければコマンドのリストは出てきます.

実はマクロの仕様も”-D”なのですがHLSLのコードをコンパイルするのと違うのが-Dのあとにスペースを空けずにマクロを書くという感じですね.

そんなわけで,拡張子.hlslでPSMain関数でピクセルシェーダを吐き出してみようというときの例としては下記のような感じですね.

glslangValidator -e PSMain -o test.spv -V -S frag -D myshader.hlsl

次に実際にある複雑そうなシェーダを試してみたいと思います.

シェーダのコンパイルテスト

とりあえず、すぐに出てくるMicrosoftのサンプルのMiniEngineのモデルビューワのシェーダコードがマクロ満載でinlcludeも満載で複雑度高いのでベンチマーク的に使ってみようかと思います.
https://github.com/Microsoft/DirectX-Graphics-Samples/tree/master/MiniEngine/ModelViewer/Shaders
全部やるのは大変なのでベーシックな以下の2つを試しましょう.

GitHubからとってきたシェーダのフォルダにglslangValidator.exeおいてみてコンパイル化k手見ます.

ModelViewerVS.hlsl

https://github.com/Microsoft/DirectX-Graphics-Samples/blob/master/MiniEngine/ModelViewer/Shaders/ModelViewerVS.hlsl
ModelViewerRS.hlsliというファイルをincludeしていますが,これはRootSignatureが書かれているだけで,シェーダ内部では使われていないので,それほど複雑ではない頂点シェーダですね.

glslangValidator.exe -e main -o testVS.spv -V -S vert -D .\ModelViewerVS.hlsl

一応,testVS.spvというのが生成されました.

ModelViewerPS.hlsl

https://github.com/Microsoft/DirectX-Graphics-Samples/blob/master/MiniEngine/ModelViewer/Shaders/ModelViewerPS.hlsl
こちらはより複雑な感じですね.includeされてるLightGrid.hlsliの中の関数は使われているようですね.

glslangValidator.exe -e main -o testPS.spv -V -S frag -D .\ModelViewerPS.hlsl

こちらも通りました.マクロも試してみようかと思いますが,とりあえず、SINGLE_SAMPLEを使うとシャドウマップのサンプリングが1タップになるようです.-Dのあとにスペース開けずにマクロ書けばOKです.

.\glslangValidator.exe -e main -o testPS.spv -V -S frag -DSINGLE_SAMPLE -D .\ModelViewerPS.hlsl

ところがこれはエラーが出ました.

ERROR: .\ModelViewerPS.hlsl:93: ‘ShadowMap’ : unknown variable
ERROR: .\ModelViewerPS.hlsl:93: ” : missing #endif
ERROR: .\ModelViewerPS.hlsl:93: ‘swizzle or member’ : Expected
ERROR: .\ModelViewerPS.hlsl:93: ‘initializer’ : Expected
.\ModelViewerPS.hlsl(93): error at column 29, HLSL parsing failed.
ERROR: 5 compilation errors. No code generated.

ShadowMapというTexture型の未定義エラーですが,実はこのコードは元のコード側のエラーですw.texShadowにでもしてあげれば通ります.

人間が読めるようにしてみる

生成された testVS.spv やtestPS.spvを開いてみるとそれらしい情報が入っている感じですが,後半コードの部分はバイナリなので読めません.これを人間が読める形にしたいと思います.
それにはオプションで”-H”を渡してみます.そうすると人間が読めるSPIR-Vのコードが吐き出されます(ファイルではなくコマンドラインに!).
とりあえず、2つのシェーダをgistに貼ってみました(長いので埋め込みません).

SPIR-Vのコードは出ているようですね.長いのでさすがに中身は追いませんw.

まとめ

とりあえず,SPIR-Vバイナリは作成できました.
ここまで読んで本当に動くの?ということに関しては,Vulkanのサンプルなどを用意した今後の話題にしたいと思います.
あと,多くの開発者がglslangで生成したバイナリのサイズや最適化などについては一段ツールをかました方がいいという話も多いのでその話題も今後にしたいと思います.