WebGL から WebGPU へ

François Beaufort
François Beaufort

WebGL デベロッパーは、WebGL の後継である WebGPU の使用を開始することに、不安と期待の両方を抱いているかもしれません。WebGPU は、最新のグラフィック API の進歩をウェブにもたらします。

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

この記事では、WebGL と WebGPU の違いをいくつか紹介し、WebGPU の入門をサポートします。

グローバル状態

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

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

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

同期を停止する

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

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

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

コンピューティング シェーダー

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

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

動画フレームの処理

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

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

// 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 コンテキストを作成し、アルファ、アンチエイリアス、カラースペース、深度、preserveDrawingBuffer、ステンシルなどのコンテキスト属性を指定すると、WebGL はキャンバスを自動的に管理します。

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

WebGPU 複数のキャンバスのデモをご覧ください。

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

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

わかりやすいエラー メッセージ

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

WebGPU のエラー メッセージは、コールスタックを提供するだけでなく、理解しやすく、対処しやすいものになっています。エラー メッセージには通常、エラーの説明とエラーの修正方法の提案が含まれています。

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

名前からインデックスへ

WebGL では、多くのものが名前で接続されています。たとえば、GLSL で myUniform という名前の uniform 変数を宣言し、gl.getUniformLocation(program, 'myUniform') を使用してその場所を取得できます。均一変数の名前を誤って入力するとエラーが発生するため、この機能は便利です。

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

Mipmap の生成

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

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

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

ユニフォーム バッファは 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 は OpenGL の規則を使用します。この規則では、Y 軸は上向きで、Z 軸はビューアの方向を向いています。WebGPU は、Y 軸が下向きで Z 軸が画面の外向きである Metal の規則を使用します。フレームバッファ座標、ビューポート座標、フラグメント/ピクセル座標では、Y 軸の方向は下向きです。クリップ空間では、WebGL と同様に Y 軸の方向は上向きです。

謝辞

この記事をレビューしてくれた Corentin Wallez、Gregg Tavares、Stephen White、Ken Russell、Rachel Andrew に感謝します。

WebGPU と WebGL の違いについて詳しく知りたい場合は、WebGPUFundamentals.org をご覧になることをおすすめします。