前回 の記事では、GPGPUのサポートという観点で、WebGLの後継としてWebGPUが必要とされる理由を述べました。
しかし、WebGPUはWebGLにコンピュートシェーダなどのモダンな機能を追加しただけのものではなく、そもそもWebGLとは設計思想が大きく異なるAPIです。
今回からは、その設計思想の違いについて考察していきます。最も大きな変化と言えるのは、リソースとステート(状態)の管理方法の違いです。
リソースとバインド
GPUに何らかの処理を依頼するとき、その処理の実行に必要なデータなどをGPUに送る必要があります。
たとえば、
- バッファ:頂点情報や数値データを格納するためのメモリ領域
- テクスチャ:画像データなどをGPUに渡すための形式
などに格納されたデータを読み込んで、もしくは、これらにデータを書き出す形で、GPU側で描画や演算などの処理を行うわけです。
WebGLやWebGPUの文脈では、このようなGPUに渡すものをすべてリソースと呼びます。
また、あるリソース(バッファやテクスチャなど)をGPUが使えるように、決められた場所に「ひもづける」操作をバインドといいます。
このバインドという操作を行って初めて、GPUはそのリソースを使えるようになります。
状態(State)
WebGLやWebGPUによって何らかの処理を実行するときには、「今、どのリソースを使って処理を行うか?」「今、どのような設定に基づいて処理を行うか?」など、現在の動作に関する設定や前提条件が常に存在しています。
- 今使っているテクスチャは何か
- 描画対象のバッファはどれか
- 有効なブレンドモードや深度設定はどうなっているか
- どのシェーダが現在アクティブか
etc.
このような、現在の動作に関する設定や前提条件は、一般にステート(状態)と呼ばれます。
WebGLやWebGPUにおいては、「現在の描画環境にどんな設定が使われているのか」がステートです。
ステートフルとステートレス
WebGLとWebGPUの最大の違いは、WebGLはステートフル(状態を保持する)APIで、WebGPUはステートレス(状態を持たない)APIだという点です。
- ステートフル:過去の設定に依存するAPI設計
- ステートレス:必要な設定を毎回明示するAPI設計
WebGLとWebGPUでは、「処理に必要な設定がどこに存在し、どう管理されるか」という、状態管理の仕組みが大きく異なります。
WebGLは状態依存
WebGLでは、たとえば次のような情報はすべてグローバルな状態として扱われています。
- どのテクスチャが現在使われているのか
- どのバッファがバインドされているのか
- どのシェーダープログラムが有効なのか
- ブレンディングやステンシルなどの設定はどうなっているか
etc.
そして、こうした状態は一度設定するとそのまま残り、明示的に変更しない限り、次の描画にも影響を与えます。
gl.bindBuffer
、gl.bindTexture()
、gl.enable
、gl.blendFunc
といったWebGLのAPIの呼び出しは、「ここから先はこの設定で描画する」というグローバルな設定の切り替えになっているのです。
そのため、WebGLでは、描画やリソース操作を行う際に、常に「今どんな状態なのか」を意識する必要があります。
もう少し具体的に見てみましょう。
たとえば、WebGLを使って、次のような描画手順を踏むとします:
gl.bindTexture(gl.TEXTURE_2D, myTexture)
gl.useProgram(myShader)
gl.drawArrays(gl.TRIANGLES, 0, 6)
このとき gl.drawArrays()
が何を描画するかは、その直前にバインドされたテクスチャやシェーダー、バッファの状態によって決まります。
つまり、WebGLでは、APIの呼び出しの意味や結果は「それ以前にどんな状態がセットされているか」によって変わってしまうのです。
このように、APIの動作が現在の状態に強く依存していることから、WebGLはステートフルなAPIと呼ばれます。
グローバル状態に依存していると何が困るのか
WebGLのAPI呼び出しによって、どのように内部のグローバルな状態が変化していくかは、次のサイトによる可視化がとても参考になります。

WebGLのAPIは、内部のグローバルな状態オブジェクト(global state
)を中心に構成されており、そこから他のオブジェクトを参照したり、参照先オブジェクトの内部を書き換えたりして、動作が決まります。
状態がこれだけ複雑に絡み合っているにもかかわらず、WebGLでは、常に「今どんな状態なのか」を意識し、APIの呼び出し順に気をつける必要があります。
状態を追跡できていないとバグに繋がる
たとえば、赤い三角形を描画した後に、緑色の三角形を描画したいとしましょう。
uniform
としてシェーダに色の情報を送り、その色を使ってシェーダ側で塗りつぶす、という手順で実現できます。
しかし、次のコードでは、赤色の三角形が2つ描画されてしまいます。
// 赤色に設定してから三角形を描画
gl.uniform4f(colorLoc, 1, 0, 0, 1) // 赤
gl.drawArrays(gl.TRIANGLES, 0, 3)
// 三角形を描画してから緑色に設定(意味がない)
gl.drawArrays(gl.TRIANGLES, 0, 3) // この描画は前の色(赤)のまま
gl.uniform4f(colorLoc, 0, 1, 0, 1) // ここで緑になる
問題は、6行目と7行目の順番です。
gl.uniform4f
で設定した色は、グローバルな状態としてそれ以降の描画に影響します。
そして、gl.drawArrays
は「今の状態に基づいて」描画します。色を設定するタイミングに注意しなければ、色を切り替えたつもりでも描画に反映させることができません。
// 赤色に設定してから三角形を描画
gl.uniform4f(colorLoc, 1, 0, 0, 1) // 赤
gl.drawArrays(gl.TRIANGLES, 0, 3)
// 緑色に設定してから三角形を描画
gl.uniform4f(colorLoc, 0, 1, 0, 1) // 緑
gl.drawArrays(gl.TRIANGLES, 0, 3)
これは簡単な例ですが、大規模なコードで同じことが起きたらどうでしょうか。
意図しない描画結果に影響していそうな状態に目星をつけて、長いコードの中から、最後にその状態が切り替わった場所を探し出さなければなりません。
状態をリセットしないと部品化できない
グローバルな状態は、バグの原因になりやすいだけでなく、コードのモジュール化や抽象化を阻むものでもあります。
ひとかたまりの描画コードをモジュールとしてまとめて再利用しようとしても、モジュールの外部で設定されている状態によって結果が変わってしまうのですから、モジュールが意図する描画結果を保証することが難しいのです。
モジュールの内部で、モジュールの役割に影響しそうな状態をすべてリセットし、使い終わったら元の状態に戻しておかないと、他のモジュールと組み合わせて使うユースケースに耐えうるものにはなりません。
場合によっては不要な状態の切り替えを行うことになるため、モジュール化によってパフォーマンス面でコストがかかることも考えられます。
WebGPUは状態を閉じ込める
一方でWebGPUには、グローバルな状態がほとんど存在しません。
代わりに、レンダーパイプライン(Render Pipeline)と呼ばれるオブジェクトに、必要な状態をすべてまとめて閉じ込める設計になっています。
WebGPUのパイプラインは、WebGLでいうグローバル状態の大部分(テクスチャや属性、バッファ、その他いろいろな設定)をまとめて持つ仕組みです。
WebGPUでは、描画を行う前にレンダーパイプラインやバインドグループなどのオブジェクトを作成し、その中にシェーダーやテクスチャ、バッファなど必要な情報をすべて詰め込みます。
そして描画時には、それを明示的に渡して実行します。
commandEncoder.setPipeline(renderPipeline)
commandEncoder.setBindGroup(0, bindGroup)
commandEncoder.draw(6, 1, 0, 0)
このコードでは、「どんなシェーダーで」「どのリソースを使って」「何を描画するか」がすべてその場で指定されており、それ以前に何をしたかには依存していません。
これが、WebGPUがステートレスなAPIと呼ばれる所以です。
状態を外に持たず、その都度必要な情報を提供することで、処理の見通しがよくなり、非同期化や最適化がしやすくなるという利点があります。
しかも、WebGPUのパイプラインは、一度作ったら変更できません(イミュータブル)。
別の設定にしたいなら、別なパイプラインを用意する必要があります。
このような特徴により、WebGPUでは、状態の変化を追跡しながらコードを書く必要がなくなります。
どの状態で描画が行われるのかが明確なので、WebGLのように「あれ?今どの状態だったっけ?」と悩むことが減るのです。
明示的で予測しやすい設計になっているのが、WebGPU APIの1つの特徴といえます。