C-WebGL: SPIR-Vのパース
シェーダーコンパイラを制作していく。といっても、GLSL言語フロントエンドとしては既にglslangがあり、最適化のバックエンドとしてSPIR-Toolsがあるので制作すべきものはそこまで多くない。
システムとしては:
(フロントエンド:glslang) → パース(リンクおよびエクスポート対象の変数の列挙) → リンク → Uniform構造体生成 → 命令パッチ → (バックエンド:SPIR-ToolsとGPUドライバ)
のような流れとなる。
SPIR-V
Vulkan(や、OpenCL、OpenGL)のGPUコードは一旦SPIR-V中間表現にコンパイルされる。実際のGPUのISAへのコンパイルはGPUドライバが行うことになる。
SPIR-Vは最初の5ワードが固定フィールドで、以降は可変長の命令を単に連結したものとなっている。
可変長といっても、命令の先頭16bitsが命令ワード数として予約されているため、命令フォーマットの知識はパースには一切不要となっている。つまり、パーサは興味のある命令の仕様だけを認識すれば十分なように配慮されている。
また、命令ストリームにはポインタが埋めこまれることが無いため、命令の境界さえ知っていれば自由に命令を加除できる。ポインタの代わりに命令は id
番号によって入出力を指定する。SPIR-VはSSA形式のIRであるため、全ての入出力にはユニークな id
が振られることになる。
インデックスの作成
可能な限り手抜きしたいので、バイナリ内で行われる各種定義をO(1)で引けるように事前にテーブルを構築しておくことにした。メモリがもったいないね。。
サポートすべき命令(Op)
次に GLSL → SPIR-Vの規格書 を眺めて、パースすべきOpを決める。この辺かな:
- OpName
- OpDecorate
- OpMemberDecorate
- OpTypePointer
- OpTypeVoid
- OpTypeInt
- OpTypeFloat
- OpTypeFunction
- OpTypeImage
- OpTypeSampler
- OpTypeVector
- OpTypeMatrix
- OpTypeSampledImage
- OpVariable
- OpConstant
- OpFunction
定数(OpConstant)とか必要なのかよという感じだが、 OpTypeArray の仕様には length
が id
で定義されている = OpConstant で定数を id
にアサインしておく必要がある というわけでConstantも収集する必要がある。。
収集したいデータ
簡単な例で見てみる:
- 頂点シェーダ とその 逆アセンブル
- フラグメントシェーダ とその 逆アセンブル
(シェーダは https://webglfundamentals.org/webgl/lessons/webgl-3d-textures.html のサンプルから取った)
main
関数
例えば、フラグメントシェーダのmainは逆アセンブルでは以下のような命令列で定義されている。(これらの命令を全て正常にパースできる必要がある)
(glslangの逆アセンブルではOpが省略されていることに注意)
OpEntryPoint によって、 ID 4
がエントリポイント(main)であることがわかる。 9
と 17
はシェーダとしての入出力の定義で、 main関数の入出力ではない 。main関数の入出力は TypeFunction
(3) → TypeVoid
(2) でvoid型の関数であることがわかる。
EntryPoint Fragment 4 "main" 9 17
Name 4 "main"
Name 9 "gl_FragColor"
Name 17 "v_texcoord"
Decorate 9(gl_FragColor) RelaxedPrecision
Decorate 17(v_texcoord) RelaxedPrecision
2: TypeVoid
3: TypeFunction 2
4(main): 2 Function None 3
5: Label
14: 11 Load 13(u_texture) // ★ main最初の命令
Uniform(通常の変数)
頂点シェーダ の
uniform mat4 u_matrix;
は、逆アセンブルでは、
Name 12 "u_matrix"
6: TypeFloat 32
7: TypeVector 6(float) 4
10: TypeMatrix 7(fvec4) 4
11: TypePointer UniformConstant 10
12(u_matrix): 11(ptr) Variable UniformConstant
のように、 OpVariable
→ OpTypePointer
→ OpTypeMatrix
→ OpTypeVector
→ OpTypeFloat
のような連鎖で定義されている。
Uniform(サンプラ/テクスチャ)
uniform sampler2D u_texture;
は、逆アセンブルでは、
Name 13 "u_texture"
Decorate 13(u_texture) RelaxedPrecision
Decorate 13(u_texture) DescriptorSet 0
Decorate 13(u_texture) Binding 0
6: TypeFloat 32
10: TypeImage 6(float) 2D sampled format:Unknown
11: TypeSampledImage 10
12: TypePointer UniformConstant 11
13(u_texture): 12(ptr) Variable UniformConstant
のように OpTypeSampledImage
と OpTypeImage
の組合せで表現される。WebGL1ではこちらの形式でしかテクスチャを与えられないが、 Vulkanも一応この形式をCombined形式としてサポートしている
attribute
頂点シェーダ の
attribute vec2 a_texcoord;
は、逆アセンブルでは、
Name 22 "a_texcoord"
6: TypeFloat 32
18: TypeVector 6(float) 2
21: TypePointer Input 18(fvec2)
22(a_texcoord): 21(ptr) Variable Input
のように、 Input
のstorage classを持つ OpVariable
で表現される。
varying
varyingは頂点シェーダとフラグメントシェーダの両方で宣言される。
varying vec2 v_texcoord;
頂点シェーダ の逆アセンブルでは、
Name 20 "v_texcoord"
6: TypeFloat 32
18: TypeVector 6(float) 2
19: TypePointer Output 18(fvec2)
20(v_texcoord): 19(ptr) Variable Output
のように、 Output
のstorage classを持つ OpVariable
で表現され、 フラグメントシェーダ では、
Name 17 "v_texcoord"
Decorate 17(v_texcoord) RelaxedPrecision
6: TypeFloat 32
15: TypeVector 6(float) 2
16: TypePointer Input 15(fvec2)
17(v_texcoord): 16(ptr) Variable Input
Input
な OpVariable
となる。フラグメントシェーダはattributeを持たないため、 Input
であれば varying
由来とわかる。
gl_Position
等の組込み変数
頂点シェーダの出力は varying
の他に gl_Position
のような組込み変数が該当する。これはSPIR-Vでは、
Name 9 "gl_Position"
Decorate 9(gl_Position) BuiltIn Position
6: TypeFloat 32
7: TypeVector 6(float) 4
8: TypePointer Output 7(fvec4)
9(gl_Position): 8(ptr) Variable Output
のように、 BuiltIn
を持つ OpDecorate
で表現される。