OpenGL Shading Language 4.60 Specification 読書ノート Part 3

仕様書該当部分

3. Basics

3.1. Character Set and Phases of Compilation

OpenGL Shading Language に使用されるソースコードを構成する文字集合は UTF-8 符号規格の Unicode だ。

前処理後の GLSL トークンストリームでは、以下の文字だけが使用可能だ:

  • [a-zA-Z_]

  • [0-9]

  • 記号各種

それ以外の文字を GLSL のトークンに使用した場合は、コンパイルエラーになる。

  • 行は、コンパイラーの診断内容や前処理器に関係する。

  • 行は、キャリッジリターンまたはラインフィードで終了する。

  • 両者が同時に使用された場合は、一つの行終端としてカウントされる。

  • この文書では、これらの組み合わせを単に改行と呼ぶことにする。

  • 行の長さは自由に設定できる。

  • 一般的に、この言語では文字集合の大文字と小文字を区別する。

  • 文字や文字列のデータ型がないため、引用符文字も含まれない。

  • ファイル終端文字もない。

より正式には、コンパイルは以下の論理的局面が順番に実行されるように行われる:

  1. ソースとなる文字列が連結され、単一の入力となる。改行はすべて保持される。

  2. 行番号は、現在のすべての改行に基づいて表示され、後に改行が削除されても変更されない。

  3. 改行の直前にバックスラッシュがある場合は、両方とも削除される(空白は置換されないので、一つのトークンが改行をまたぐことができる)。新たに生成されたバックスラッシュと改行は取り除かれず、局面 1. 以降に最初に生成された対しか取り除かれない。

  4. すべてのコメントは単一の空白文字で置き換えられる。ただし、// スタイルのコメントは、終了する改行の前で終わり、空白は一般的に前処理に関係する。

  5. 前処理が行われ、上記の文字集合から形成された GLSL トークンの一連の並びが得られる。

  6. 一連の GLSL トークンに対して GLSL 処理が行われる。

3.2. Source Strings

単一のシェーダーに対するソースとは、文字集合の文字列の配列だ。単一のシェーダーはこれらの文字列を連結したものからなる。各文字列は改行で区切られた複数の行を含むことができる。一つの文字列の中に改行がある必要はなく、複数の文字列から単一行を形成することができる。文字列を連結して単一シェーダーを形成する際には、実装は改行やその他の文字を挿入しない。複数のシェーダーをリンクして単一のプログラムにすることができる。

シェーダーのコンパイルから返される診断内容は、文字列内の行番号と、その伝達内容がどのソース文字列に該当するかの両方を特定する必要がある。ソース文字列は、最初の文字列を文字列 0 として順次カウントされる。行番号は、処理された新しい行の数よりも一つ多くなるが、これには、行継続文字によって削除される新しい行をカウントすることも含まれる。

改行 (a new-line) の前にあって、行継続文字により区切られた行(複数形)は、コメント処理や前処理の前に連結される。これは行継続文字の代わりに空白文字が使われることはないことを意味する。すなわち、ある行の終わりの文字とその次の行の始まりの文字を連結することで、単一トークンが形成される。

// equivalent to "float foo;"
float f\
oo;

読者ノート

Pygments (2.9.0) は上記コード片を正しくハイライトできない。

3.3. Preprocessor

コンパイルの一環としてソース文字列を処理する前処理器がある。後で記す点を除けば C++ 標準の前処理器として動作する。

読者ノート

指示子の一覧省略。

各記号 # は、その行の前に空白またはタブしか置くことができない。また、空白やタブを、指令の前に置くこともできる。各指令は改行で終了する。前処理では、文字列中の改行の数や相対的な位置は変更されない。前処理は、行継続文字によって改行が取り除かれた後に行われる。

行中の記号 # は無視される。上記以外の指令は、コンパイル時にエラーになる。

C++ 前処理器の標準機能である #define および #undef 機能は、マクロパラメーターを持つマクロ定義と持たないマクロ定義の両方で定義される。


以下の定義済みマクロが用意されている:

__LINE__
__FILE__
__VERSION__

__LINE__ は、現在のソース文字列における先行する改行数よりも 1 多い十進数の整数定数に置換される。

__FILE__ は、どのソース文字列番号が現在処理されているのかを示す十進数の整数定数に置換される。

__VERSION__ は、OpenGL シェーディング言語のバージョン番号を反映した十進数の整数に置換される。この文書によるシェーディング言語のバージョンは 460 に置換される。

慣習上、二つ以上の連続したアンダースコアを含むすべてのマクロ名は、下位のソフトウェア層が使用するために予約済みだ。シェーダー内でこのような名前を定義したり定義解除したりしても、それ自体がエラーになることはないものの、同じ名前が複数定義されていることに起因する意図しない動作が発生する可能性がある。また、GL_ から始まるマクロ名もすべて予約済みで、このような名前を定義したり、定義解除したりすると、コンパイル時にエラーになる。

実装では、マクロ名の長さを最大 1024 文字まで対処する必要がある。実装では、1024 文字を超える長さのマクロ名に対してエラーを生成することも、そのようなマクロ名を対処することも許可されている。


#if, #ifdef, #ifndef, #else, #elif, #endif は、以下の点を除き、C++ 前処理器の標準的な動作をするように定義されている:

  • #if および #elif に続く式は次の二つに制限される:

    • リテラルの整数定数と、

    • 演算子 defined によって消費される識別子を操作する式

  • 文字定数は対処されていない。


使用できる演算子の表省略。


演算子 defined は、以下のいずれかの方法で使用できる:

defined identifier
defined ( identifier )

マクロ内の二つのトークンは C++ 前処理器で標準的に使用されているトークン貼り付け演算子 ## を使用して一つのトークンに連結できる。結果は一つの有効トークンでなければならず、そのトークンはマクロ展開の対象となる。つまり、マクロ展開はトークン貼り付けの後にしか行われない。その他の数字記号に基づく演算子 (#, #@, etc.) はなく、演算子 sizeof もない。

前処理器で整数リテラルに演算子を適用する際の意味論は OpenGL Shading Language のものではなく、C++ 前処理器の標準的なものと一致する。

前処理器の式は、シェーダーが対象とする処理器ではなく、ホスト処理器の動作に従って評価される。

#error が発生すると、実装はコンパイル時の診断内容をシェーダーオブジェクトの情報ログに記録する。伝達内容は #error 指令に続くトークンで、最初の改行までとなる。実装では #error 指令の存在をコンパイルエラーとして扱わなければならない。

#pragma は、実装依存コンパイラー制御を許す。#pragma に続くトークンは、前処理器のマクロ展開の対象にはならない。実装が #pragma に続くトークンを認識しない場合は、それを無視する。次の pragmas は言語の一部として定義されている:

#pragma STDGL

STDGL pragma は、この言語の将来の改訂版で使用するための pragma を予約するために使用される。どのような実装でも、最初のトークンが STDGL である pragma を使用することは許さない。

#pragma optimize(on)
#pragma optimize(off)

optimize pragma はシェーダーの開発やデバッグを支援するために、最適化を切るために使用できる。これは、関数定義の外側でしか使用できない。既定では、すべてのシェーダーで optimize がオンになっている。

#pragma debug(on)
#pragma debug(off)

debug pragma はデバッグ情報を付加してシェーダーをコンパイルし、デバッガーでそれを使用できるようにするために使用される。この pragma は、関数定義の外側でしか使用できない。既定では debug は効いていない。

シェーダーは言語のバージョンを宣言する必要がある。それは次のように指定される:

#version number profile_opt

ここで number は言語のバージョンでなければならず、上記の __VERSION__ と同じ規則に従う。言語のバージョン 4.60 を使用するシェーダーでは #version 460 という指令が必要だ。コンパイラーが対処していない言語のバージョンを表す数字を入力すると、コンパイルエラーが発生する。言語のバージョン 1.10 では、シェーダーにこの指令を含める必要はない。#version 指令を含まないシェーダーは、バージョン 1.10 を対象としているものとして扱われる。バージョン 100, 300, 310 を指定したシェーダーは、OpenGL ES Shading Language のバージョン 1.00, 3.00, 3.10 を対象としたものとしてそれぞれ扱われる。

読者ノート

WebGL をやるときに特にこの知識が必要になる。

オプションの profile 引数を指定する場合は、OpenGL プロファイルの名前である必要がある。現在、選択肢が三つある:

core
compatibility
es

引数 profile はバージョン 150 以上でしか使用できない。引数 profile が指定されておらず、バージョンが 150 以上の場合、既定は core だ。バージョン 300 または 310 が指定されている場合、引数 profile はオプションではなく es でなければならず、そうでない場合はコンパイルエラーとなる。es プロファイルの言語仕様は The OpenGL ES Shading Language specification で規定されている。

異なるバージョンを宣言している core または compatibility プロファイルのシェーダーを一緒にリンクすることができる。ただし、es プロファイルのシェーダーを非 es プロファイルのシェーダーやバージョンの異なる es プロファイルのシェーダーとリンクすることはできず、リンクエラーとなる。これらの規則で許可されたバージョンのシェーダーをリンクする場合、残りのリンクエラーは、シェーダーがリンクされているコンテキストのバージョンに対応するGLSL バージョンのリンク規則に従って与えられる。シェーダーのコンパイルエラーは、各シェーダー内で宣言された(または既定で設定された)バージョンに厳密に基づいて与えられなければならない。

特に指定のない限り、本仕様書はコアプロファイルを文書化しており、コアプロファイルに指定されているものはすべて互換性プロファイルでも利用可能だ。互換性プロファイルに特に属すると指定された機能は、コアプロファイルでは利用可能でない。互換性プロファイルの機能は SPIR-V の生成時には利用可能でない。

実装が対処する各プロファイルには組み込みマクロ定義がある。すべての実装には次のマクロが用意されている:

#define GL_core_profile 1

互換性プロファイルを提供する実装には次のマクロが用意されている:

#define GL_compatibility_profile 1

es プロファイルを提供する実装では、次のマクロが用意されている:

#define GL_es_profile 1

シェーダーの中では、コメントや空白を除いて、何よりも先に #version 指令を記述しなければならない。

既定では、この言語のコンパイラーは、この仕様に適合していないシェーダーに対してコンパイル時に字句エラーや文法エラーを出さなければならない。どんな拡張された動作も、最初に有効にする必要がある。拡張機能に関するコンパイラーの動作を制御するための指令は #extension 指令で宣言される:

#extension extension_name : behavior
#extension all : behavior

ここで extension_name は拡張の名前だ。拡張の名前は当仕様では文書化されていない。トークン all はその動作がコンパイラーで対処されている拡張子すべてに適用されることを意味する。動作は以下のいずれかになる:

require

拡張 extension_name で指定されたとおりに動作する。

拡張 extension_name が対処されていない場合や、all が指定されている場合は #extension に対するコンパイルエラーとなる。

enable

拡張 extension_name で指定されたとおりに動作する。

拡張 extension_name が対処されていない場合 #extension に警告する。all が指定されている場合は #extension に対するコンパイルエラーとなる。

warn

拡張 extension_name で指定されたとおりに動作する。ただし、他の有効または必須の拡張で対処されている場合を除き、その拡張の使用が検出された場合は警告を発する。

all が指定された場合、使用されているすべての拡張の検出可能な使用に対して警告する。

拡張 extension_name が対処されていない場合は #extension に対して警告する。

disable

拡張 extension_name が言語定義に含まれていないかのような動作(エラーや警告を含む)をする。

all が指定された場合は、コンパイル先の言語の拡張されていないコアバージョンの動作に撤回しなければならない。

拡張 extension_name が対処されていない場合は #extension に対して警告する。

extension 指令は、各拡張の動作を設定するための単純で低水準の仕組みだ。どのような組み合わせが適切なのかといったポリシーは定義しない。各拡張の動作を設定する際には、指令の順番が重要だ。遅れて出てきた指令は早く出てきたものを上書きする。 all の変種は拡張すべてに対する動作を設定し、以前に発令されたすべての拡張機能の指令を上書きするが、warndisable の動作しか上書きされない。

コンパイラーの初期状態は、あたかも指令:

#extension all : disable

すべてのエラーや警告の報告は、この仕様に基づいて行われなければならず、拡張は無視されることをコンパイラに伝える。が発令されたかのように、「すべてのエラーや警告の報告はこの仕様に基づいて行われなければならず、拡張は無視される」ことをコンパイラーに教える。

各拡張は、許容されるスコープの粒度を定義することができる。何も言われなければ、粒度はシェーダー(つまり単一コンパイル単位)で、拡張指示は前処理器以外のトークンの前でなければならない。必要であれば、リンカーは単一コンパイル単位よりも大きな粒度を強制することができる。その場合、関係する各シェーダーは必要な拡張指令を含まなければならない。

マクロの展開は #extension#version 指令を含む行では行われない。

#line はマクロ置換後、次のいずれかの形式とならなければならない:

#line line
#line line source-string-number

ここで linesource-string-number は定整数式だ。これらの定数式が整数リテラルでない場合の動作は未定義だ。この指令(改行を含む)を処理した後、実装はまるで行番号 line とソース文字列番号 source-string-number でコンパイルしているかのように動作する。後続のソース文字列は、他の #line 指令がその番号を上書きするまで、連続して番号が付けられる。

Note

#line 指令の中で定数表現を許可している実装と、そうでない実装がある。式が対処される場合でも、文法が曖昧なので、結果は実装依存となる。例えば:

#line +2 +2 // Line number set to 4, or file to 2 and line to 2

OpenGL SPIR-V 用にシェーダーをコンパイルした場合、次の定義済みマクロが利用できる:

#define GL_SPIRV 100

Vulkan を対象にする場合、次の定義済みマクロが利用できる:

#define VULKAN 100

3.4. Comments

コメントは /**/、または // と改行で区切られる。コメント開始時の区切りパターンは、コメント内ではそれとして認識されないため、コメントを入れ子にすることはできない。コメント /* はコメント終了時の区切りパターン */ を含む。しかし // コメントは終端の改行を含まない(つまり排除する)。

コメント内では、値が 0 のバイトを除き、任意のバイト値を使用することができる。コメントの内容については、エラーは発生せず、コメントの内容を検証する必要もない。

コメントが処理される前に、論理的には行継続文字による改行の除去が行われる。つまり、文字 \ で終わる単一行コメントは、次の行も含めてコメントになる。

// a single-line comment containing the next line \
a = b; // this is still in the first comment

3.5. Tokens

前処理を終えた言語は、トークンの順序のある並びだ。

token :
keyword
identifier
integer-constant
floating-constant
operator
; { }

3.6. Keywords

この節の前半にある一覧が当言語のキーワードであり、前処理以降はこの仕様書に記載されているとおりにしか使用できず、そうでない場合はコンパイル時にエラーが発生する。

Vulkan を対象にする場合には追加のキーワードが存在する。

さらに、将来使用するために予約されてるキーワードが多数定義されている。これらを使用すると、コンパイルエラーが発生する。

その上、前述のダブルアンダースコア規則が適用される。

3.7. Identifiers

識別子は、変数名、関数名、構造体名、フィールドセレクター(構造体のメンバーと同様に、フィールドセレクターはベクトルや行列の構成要素を選択する)に使用される。

読者ノート

識別子の BNF みたいな表がここにあるが省略。

  • gl_ で始まる識別子は予約されており、一般的にはシェーダ内で宣言することはできない。

  • 前述の 1024 文字ルールがここでも適用される。

3.8. Definitions

後述する言語規則のいくつかは、次の定義に依存する。

3.8.1. Static Use

シェーダーに変数 x静的に使用されている (a static use) のは、前処理後にシェーダーに x の任意の部分にアクセスするような文が含まれている場合であり、制御の流れによってその文が実行されるかどうかには関係ない。このような変数は、 静的に使用されている (statically used) と呼ばれる。アクセスが書き込みの場合、x静的に割り当てられている (statically assigned) とも言われる。

3.8.2. Dynamically Uniform Expressions and Uniform Control Flow

一部の操作では、式が 動的に一様である (dynamically uniform) ことや、一様 な制御フロー (uniform control flow) の中に配置されていることが要求される。これらの要件は以下の定義集合で定義されている。

呼び出し (an invocation) とは、特定の段階における main() の単一実行のことあって、その段階のシェーダー内で明示的に公開されているデータ量に対してしか作用しない(データの追加的なインスタンスに対する暗黙の操作は、追加的な呼び出しとなる)。例えば、計算実行モデルでは、単一の呼び出しが単一の作業項目に対してしか作用せず、頂点実行モデルでは、単一の呼び出しが単一の頂点に対してしか作用しない。

呼び出しグループ (an invocation group) とは、特定の計算作業グループまたはグラフィック操作をまとめて処理する呼び出しの完全な集合だ。「グラフィック操作」の範囲は実装に依存するが、クライアント API で定義されているように、少なくとも単一の三角形またはパッチと同じ大きさであり、最大でも一つのレンダリング命令と同じ大きさだ。

単一の呼び出しで、単一のシェーダー文が複数回実行され、その命令の 動的インスタ ンス (dynamic instances) が複数得られる。これは、命令がループ内で実行される場合や、複数の呼び出し場所から呼び出される関数内で実行される場合、あるいはこれらの複数の組み合わせで発生する。ループの繰り返しや、関数と呼び出し場所の動的な連鎖が異なると、そのような命令の動的インスタンスも異なる。動的インスタンスは、どの呼び出しが実行されたかではなく、呼び出し内の制御フローの経路によって区別される。つまり、main() の異なる呼び出しは、同じ制御フロー経路をたどる場合、その命令の同じ動的インスタンスを実行する。

ある式がそれを消費するある動的インスタンスに対して 動的に一様 (dynamically uniform) であるとは、動的インスタンスを実行する(呼び出しグループ内の)呼び出しすべてに対してその値が同じであるときに言う。

一様制御フロー (収束制御フロー)は、呼び出しグループ内のすべての呼び出しが同じ制御フロー経路(したがって、命令の動的インスタンスの順序も同じ)を実行するときに発生する。一様制御フローは main() に入ったときの初期状態であり、条件分岐が異なる呼び出しに対して異なる制御経路を取るまで続く(非一様制御フローまたは発散制御フロー)。このような発散は再収束し、すべての呼び出しが再び同じ制御フローの経路を実行するようになり、これにより一様制御フローの存在が再び確立される。選択肢やループに入ったときに制御フローが一様であり、その後、呼び出しグループのすべての呼び出しがその選択肢やループから離れると、制御フローは一様に収束し直す。

main()
{
    float a = ...; // this is uniform control flow
    if (a < b) {   // this expression is true for some fragments, not all
        ...;       // non-uniform control flow
    } else {
        ...;       // non-uniform control flow
    }
    ...;           // uniform control flow again
}

定数式は動的に一様であることは自明だ。これにより、定数式に基づく典型的なループ計数器も動的に一様であることがわかる。

読者ノート

解析の教科書のような文章に読み取ってしまった。