POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

FeedlyRSSTwitterFacebook
Adrian Sampson

本記事は、原著者の許諾のもとに翻訳・掲載しております。

リアルタイムのグラフィックAPIのメインストリームであるOpenGLとDirect3Dは、プログラマが異なるハードウェアと相互にやりとりする方法としては恐らく最も広く利用されているものでしょう。しかし、これらの行うCPU-GPUインテグレーションの品質は到底受け入れがたいものです。良いパフォーマンスを実現するためには、CPU側のコードとGPU側のシェーダプログラムを緊密に調整する必要がありますが、今のAPIではCPUとGPUは独立して実行するものとして扱われています。そのため、文字列型のインターフェイスや大量のボイラープレート、貧しいGPU固有のプログラミング言語が存在することになってしまうのです。

この投稿では、必ずしも愉快とは思えないOpenGLアプリの些細な現実をいくつかお見せします。 プログラムリスト全ソースコード を参照しながらお読みください。

シェーダは文字列

3Dでオブジェクトの外見を定義する場合、リアルタイムコンピュータグラフィックスでは シェーダ と呼ばれるアプリケーションが使用されます。これは、レンダリングパイプラインの一部としてGPU上で実行させる小さいプログラムです。シェーダにはいくつか 種類 がありますが、最もよく使用されるのは、オブジェクトのメッシュの頂点の位置を特定する 頂点シェーダ とオブジェクトの表面色のピクセルを作成する フラグメントシェーダ です。シェーダを書く時に特別なC言語に似たプログラミング言語が使用されます。OpenGLでは GLSL というシェーダ専用言語が使用されます。

これがうまくいかない原因となるのです。シェーダを設定するために、ホストプログラムが、 シェーダのソースコードを含む文字列 をグラフィックスカードドライバへと送信します。ドライバは実行時にソースコードをGPUの内部アーキテクチャにコンパイルし、ハードウェアにロードします。

簡略化したGLSL 頂点シェーダとフラグメントシェーダのC言語文字列リテラル のペアは次のようになります。

const char *vertex_shader =
  "in vec4 position;\n"
  "out vec4 myPos;\n"
  "void main() {\n"
  "  myPos = position;\n"
  "  gl_Position = position;\n"
  "}\n";

const char *fragment_shader =
  "uniform float phase;\n"
  "in vec4 myPos;\n"
  "void main() {\n"
  "  gl_FragColor = ...;\n"
  "}\n";

(起動時にテキストファイルからシェーダコードをロードする方法も一般的です) in out uniform 修飾子 は、CPUとGPUの間やGPUの異なる段階にあるレンダリングパイプラインの間の通信チャネルを示します。 myPos 変数は、頂点シェーダからフラグメントシェーダへとデータを渡すのに役立ちます。頂点シェーダの main 関数は gl_Position という魔法の組み込み変数を出力のために割り当て、フラグメントシェーダは、 gl_FragColor 変数を割り当てます。

シェーダプログラムをコンパイルしてロードする 方法は次のようになります。

// Compile the vertex shader.
GLuint vshader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vshader, 1, &vertex_shader, 0);

// Compile the fragment shader.
GLuint fshader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fshader, 1, &fragment_shader, 0);

// Create a program that stitches the two shader stages together.
GLuint shader_program = glCreateProgram();
glAttachShader(shader_program, vshader);
glAttachShader(shader_program, fshader);
glLinkProgram(shader_program);

このボイラープレートを使って shader_program を呼び出してオブジェクトを描画する準備ができました。

文字列シェーダのインターフェイスはグラフィックスプログラミングの原罪なのです。つまり、完全なプログラムのセマンティックスの一部はランタイムまで知ることはできないことを意味します。これには、異なるハードウェアを標的にしていること以外に特に理由はありません。これは JavaScriptの eval 関数 のようですが、これ以上に面倒なのは、全てのOpen GLプログラムではコードを文字列に詰め込むことが 必要 になるのです。

Direct3Dや次世代のグラフィックスAPIである MantleMetalVulkan は、生のソースコードの代わりにバイトコードを使用してシェーダを実行することで、きれいなプログラムにできます。しかし、IRへのプリコンパイルシェーダープログラムは根本的な問題を解決してはいません。その問題とは、「CPUコードとGPUコードとの間の インターフェイス が無駄に動的であるため、異種のプログラム全体を静的に論証できない」というものです。

文字列に型付けされたバインディングのボイラープレート

文字列でラップされたシェーダコードがOpenGLの自己資金投資の痛みなら、CPUとGPUの通信インターフェイスを介して、痛みの配当金を集めます。

頂点シェーダの position 変数とフラグメントシェーダの phase 変数をそれぞれ確認してみてください。 in 修飾子と uniform 修飾子はCPUからのパラメータです。このパラメータを使用するには、ホストプログラムはまず各変数の 位置情報を探します

GLuint loc_phase = glGetUniformLocation(program, "phase");
GLuint loc_position = glGetAttribLocation(program, "position");

変数の名前を文字列として渡して探します。 phase パラメータは単にスカラー型の float ですが、 position は位置ベクトルの動的配列のため、 バックバッファを設定する には、さらにボイラープレートが必要になります。

次にこれらハンドルを使用して シェーダにデータを渡し 、それぞれのフレームを描画します。

// The render loop.
while (1) {
  // Set the scalar `phase` variable.
  glUniform1f(loc_phase, sin(4 * t));

  // Set the `location` array by copying data into the buffer.
  glBindBuffer(GL_ARRAY_BUFFER, buffer);
  glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(points), points);

  // Use these parameters and our shader program to draw something.
  glUseProgram(shader_program);
  glDrawArrays(GL_TRIANGLE_FAN, 0, NVERTICES);
}

Verbose モード(詳細な起動ログを出力するモード) によって気をそらされますが、 glUniform1f glBufferSubData での呼び出しは let variable = value の代わりに set("variable", value) と書くのと同じことになのです。C言語やGLSLのコンパイラはCPUとGPUコードを別々にチェックし最適化してくれます。文字列型のCPUとGPUのインターフェイスはそれぞれのコンパイラがプログラム全体に影響を与えないようにしてくれます。

異質性の時代

ハードウェア異質性の時代においては、OpenGLやこれに相当するものは哀れな主唱者となってしまいます。異質性は急速にユビキタスになってきています。そのため、異なる機能を持つハードウェアに対応できるソフトウェアを書くためのより良い方法が必要とされています。OpenGLのプログラミングモデルは、「異質のソフトウェアは、疎結合な複数の個別プログラムで構成されるべき」という単純な観点を重視しています。

説得力のある異質性が成功するには、20世紀的見解を捨てる必要があります。必要となるプログラミングモデルは、複数の実行内容を可能にする 1つ のプログラムを書くことができることなのです。しかし、異質性の複雑さの本質をなくすことはできません。しかし、CPUコード以外のものをもう少し重んじることができるのではないでしょうか。

監修者
監修者_古川陽介
古川陽介
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
複合機メーカー、ゲーム会社を経て、2016年に株式会社リクルートテクノロジーズ(現リクルート)入社。 現在はAPソリューショングループのマネジャーとしてアプリ基盤の改善や運用、各種開発支援ツールの開発、またテックリードとしてエンジニアチームの支援や育成までを担う。 2019年より株式会社ニジボックスを兼務し、室長としてエンジニア育成基盤の設計、技術指南も遂行。 Node.js 日本ユーザーグループの代表を務め、Node学園祭などを主宰。