WebGL から WebGPU へ

François Beaufort
François Beaufort

WebGL デベロッパーにとって、WebGPU の使用を開始するのは、恐怖と興奮の両方を感じるかもしれません。WebGPU は、最新のグラフィック API の進歩をウェブにもたらす、WebGL の後継です。

WebGL と WebGPU は多くのコアコンセプトを共有しています。どちらの API でも、シェーダーと呼ばれる小さなプログラムを GPU 上で実行できます。WebGL は頂点シェーダーとフラグメント シェーダーをサポートしていますが、WebGPU はコンピューティング シェーダーもサポートしています。WebGL は OpenGL シェーディング言語(GLSL)を使用し、WebGPU は WebGPU シェーディング言語(WGSL)を使用します。2 つの言語は異なりますが、基本的なコンセプトはほとんど同じです。

本記事では、WebGL と WebGPU の違いをいくつか取り上げ、使い始める際の参考にしていただければ幸いです。

グローバル状態

WebGL には多くのグローバル ステータスがあります。一部の設定は、どのテクスチャとバッファがバインドされるかなど、すべてのレンダリング オペレーションに適用されます。このグローバル状態は、さまざまな API 関数を呼び出して設定します。この状態は、変更するまで有効です。WebGL のグローバル状態は、グローバル設定の変更を忘れがちであるため、エラーの主要な原因となります。また、グローバル状態を使用するとコードの共有が難しくなります。これは、コードの他の部分に影響を与えるような方法でグローバル状態を誤って変更しないように、デベロッパーが注意する必要があるためです。

WebGPU はステートレス API であり、グローバルな状態を維持しません。代わりに、パイプラインのコンセプトを使用して、WebGL でグローバルだったすべてのレンダリング状態をカプセル化します。パイプラインには、使用するブレンド、トポロジ、属性などの情報が含まれます。パイプラインは不変です。設定を変更する場合は、別のパイプラインを作成する必要があります。WebGPU は、コマンド エンコーダを使用してコマンドをバッチ処理し、記録された順序で実行します。これは、オブジェクトを 1 回通過するだけで、ライトのシャドウマップごとに 1 つずつ、複数のコマンド ストリームをアプリケーションが記録できるシャドウ マッピングで役立ちます。

まとめると、WebGL のグローバル状態モデルでは、堅牢でコンポーザブルなライブラリとアプリケーションの作成が困難で脆弱でしたが、WebGPU では、デベロッパーが GPU にコマンドを送信する際に追跡する必要のある状態の量が大幅に削減されました。

同期を停止する

GPU では、パイプラインをフラッシュしてバブルが発生する可能性があるため、通常、コマンドを送信して同期的に待機することは非効率的です。これは、マルチプロセス アーキテクチャを使用し、GPU ドライバが JavaScript とは別のプロセスで実行される WebGPU と WebGL で特に当てはまります。

たとえば、WebGL で gl.getError() を呼び出すには、JavaScript プロセスから GPU プロセスへの同期 IPC とその逆が必要です。これにより、2 つのプロセスが通信するときに CPU 側でバブルが発生する可能性があります。

このようなバブルを回避するため、WebGPU は完全に非同期になるように設計されています。エラーモデルと他のすべてのオペレーションは非同期で実行されます。たとえば、テクスチャを作成すると、テクスチャが実際にはエラーであっても、オペレーションはすぐに成功したように見えます。エラーは非同期でのみ検出できます。この設計により、プロセス間通信のバブルが発生せず、アプリケーションの信頼性の高いパフォーマンスが実現します。

コンピュート シェーダー

コンピューティング シェーダーは、GPU 上で実行され、汎用計算を行うプログラムです。これらは WebGL ではなく、WebGPU でのみ使用できます。

頂点シェーダーやフラグメント シェーダーとは異なり、グラフィック処理に限定されず、機械学習、物理シミュレーション、科学コンピューティングなど、さまざまなタスクに使用できます。コンピューティング シェーダーは数百、数千ものスレッドによって並列に実行されるため、大規模なデータセットの処理に非常に効率的です。GPU コンピューティングの詳細については、WebGPU に関する詳細な記事をご覧ください。

動画フレーム処理

JavaScript と WebAssembly を使用してビデオ フレームを処理することには、いくつかの欠点があります。GPU メモリから CPU メモリにデータをコピーするコストと、ワーカーと CPU スレッドで実現できる並列処理の制限です。WebGPU にはこのような制限がなく、WebCodecs API と緊密に統合されているため、動画フレームの処理に適しています。

次のコード スニペットは、VideoFrame を WebGPU で外部テクスチャとしてインポートして処理する方法を示しています。こちらのデモをお試しいただけます。

// Init WebGPU device and pipeline...
// Configure canvas context...
// Feed camera stream to video...

(function render() {
  const videoFrame = new VideoFrame(video);
  applyFilter(videoFrame);
  requestAnimationFrame(render);
})();

function applyFilter(videoFrame) {
  const texture = device.importExternalTexture({ source: videoFrame });
  const bindgroup = device.createBindGroup({
    layout: pipeline.getBindGroupLayout(0),
    entries: [{ binding: 0, resource: texture }],
  });
  // Finally, submit commands to GPU
}

デフォルトでのアプリケーションのポータビリティ

WebGPU では、limits をリクエストする必要があります。デフォルトでは、requestDevice() は、物理デバイスのハードウェア機能と一致しない可能性のある GPUDevice を返します。これは、すべての GPU の妥当な最小公倍数です。デベロッパーにデバイスの上限をリクエストするよう義務付けることで、WebGPU はアプリケーションが可能な限り多くのデバイスで実行されるようにします。

キャンバスの処理

WebGL コンテキストを作成して、alpha、antialias、 colorSpace、depth、preserveDrawingBuffer、stencil などのコンテキスト属性を指定すると、WebGL はキャンバスを自動的に管理します。

一方、WebGPU ではキャンバスを自分で管理する必要があります。たとえば、WebGPU でアンチエイリアシングを実現するには、マルチサンプリング テクスチャを作成してレンダリングします。次に、マルチサンプリング テクスチャを通常のテクスチャに解決し、そのテクスチャをキャンバスに描画します。この手動管理により、1 つの GPUDevice オブジェクトから任意の数のキャンバスに出力できます。一方、WebGL ではキャンバスごとに作成できるコンテキストは 1 つだけです。

WebGPU マルチキャンバスのデモをご覧ください。

なお、現在、ブラウザではページあたりの WebGL キャンバスの数に上限があります。執筆時点では、Chrome と Safari では最大 16 個の WebGL キャンバスしか同時に使用できません。Firefox では最大 200 個のキャンバスを作成できます。一方、ページあたりの WebGPU キャンバスの数に制限はありません。

Safari、Chrome、Firefox ブラウザの WebGL キャンバスの最大数を示すスクリーンショット
Safari、Chrome、Firefox の WebGL キャンバスの最大数(左から右)- デモ

役立つエラー メッセージ

WebGPU は、API から返されるすべてのメッセージのコールスタックを提供します。つまり、コード内でエラーが発生した場所をすばやく確認できるため、デバッグやエラーの修正に役立ちます

WebGPU のエラー メッセージは、コールスタックを提供するだけでなく、わかりやすく、対処も容易です。通常、エラー メッセージにはエラーの説明と、エラーを修正するための推奨事項が含まれています。

WebGPU では、各 WebGPU オブジェクトにカスタムの label を指定することもできます。このラベルは、GPUError メッセージ、コンソール アラート、ブラウザのデベロッパー ツールでブラウザによって使用されます。

名前からインデックスへ

WebGL では、多くのものが名前で接続されています。たとえば、GLSL で myUniform というユニフォーム変数を宣言し、gl.getUniformLocation(program, 'myUniform') を使用してその場所を取得できます。これは、ユニフォーム変数の名前を間違って入力した場合にエラーが表示されるため便利です。

一方、WebGPU では、すべてがバイトオフセットまたはインデックス(多くの場合「ロケーション」と呼ばれます)によって完全に接続されます。WGSL と JavaScript のコード位置を同期するのは、お客様の責任となります。

Mipmap の生成

WebGL では、テクスチャのレベル 0 mip を作成してから gl.generateMipmap() を呼び出すことができます。その後、WebGL が他のすべての MIP レベルを生成します。

WebGPU では、ミップマップを自分で生成する必要があります。これを実行する組み込み関数はありません。この決定について詳しくは、仕様に関するディスカッションをご覧ください。webgpu-utils などの便利なライブラリを使用して MIP マップを生成することも、自分で作成する方法を学ぶこともできます。

ストレージ バッファとストレージ テクスチャ

ユニフォーム バッファは WebGL と WebGPU の両方でサポートされており、サイズが制限された定数パラメータをシェーダーに渡すことができます。ストレージ バッファはユニフォーム バッファによく似ていますが、WebGPU でのみサポートされており、ユニフォーム バッファよりも強力で柔軟です。

  • シェーダーに渡されるストレージ バッファのデータは、ユニフォーム バッファよりもはるかに大きくなる場合もあります。仕様では、均一バッファ バインディングのサイズは最大 64 KB とされています(maxUniformBufferBindingSize を参照)。一方、WebGPU ではストレージ バッファ バインディングの最大サイズは 128 MB 以上です(maxStorageBufferBindingSize を参照)。

  • ストレージ バッファは書き込み可能で、一部のアトミック オペレーションをサポートしますが、均一バッファは読み取り専用です。これにより、新しいクラスのアルゴリズムを実装できます。

  • ストレージ バッファ バインディングは、より柔軟なアルゴリズムのためにランタイム サイズの配列をサポートしていますが、ユニフォーム バッファ配列のサイズはシェーダーで指定する必要があります。

ストレージ テクスチャは WebGPU でのみサポートされており、ストレージ バッファがユニフォーム バッファに対してどのような関係にあるかと同じ関係でテクスチャに対して存在します。通常のテクスチャよりも柔軟で、ランダム アクセス書き込み(および将来的には読み取り)をサポートしています。

バッファとテクスチャの変更

WebGL では、バッファまたはテクスチャを作成してから、gl.bufferData()gl.texImage2D() を使用してサイズをいつでも変更できます。

WebGPU では、バッファとテクスチャは不変です。つまり、作成後にサイズ、使用状況、形式を変更することはできません。変更できるのはコンテンツのみです。

スペースの規則の違い

WebGL では、Z クリップ空間の範囲は -1 ~ 1 です。WebGPU では、Z クリップ空間の範囲は 0 ~ 1 です。つまり、z 値が 0 のオブジェクトはカメラに最も近く、z 値が 1 のオブジェクトは最も遠くにあることになります。

WebGL と WebGPU の Z クリップ空間範囲のイラスト。
WebGL と WebGPU の Z クリップ空間の範囲。

WebGL では、Y 軸が上、Z 軸が視聴者側を向くという OpenGL の規則が使用されます。WebGPU は Metal の規則を使用します。Y 軸は下、Z 軸は画面外です。フレームバッファ座標、ビューポート座標、フラグメント/ピクセル座標では、Y 軸の向きは下向きです。クリップ空間では、Y 軸の向きは WebGL と同じく上です。

謝辞

この記事のレビューにご協力いただいた Corentin Wallez、Gregg Tavares、Stephen White、Ken Russell、Rachel Andrew の各氏に感謝します。

また、WebGPU と WebGL の違いについて詳しくは、WebGPUFundamentals.org をご覧ください。