WebGL から WebGPU へ

フランソワ ボーフォール
François Beaufort

WebGL のデベロッパーの皆様は、最新のグラフィック API をウェブにもたらす WebGL の後継である WebGPU の利用を開始することについて、不安を感じているかもしれません。

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

この記事では、その点を念頭に置いて、WebGL と WebGPU の違いをいくつか紹介します。

グローバルの状態

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

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

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

これ以上同期しない

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

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

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

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

コンピューティング シェーダーは、GPU で実行されるプログラムであり、汎用的な計算を行います。これは WebGPU でのみ使用できます。WebGL では使用できません。

頂点シェーダーやフラグメント シェーダーとは異なり、グラフィック処理に限定されず、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 コンテキストを作成し、alpha、アンチエイリアス、colorSpace、depth、repreveDrawingBuffer、ステンシルなどのコンテキスト属性を指定すると、WebGL は自動的にキャンバスを管理します。

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

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

ちなみに、ブラウザは現在、ページあたりの WebGL キャンバスの数に上限があります。本記事の執筆時点で、Chrome と Safari で同時に使用できる WebGL キャンバスは最大 16 です。Firefox では、最大で 200 の WebGL キャンバスを作成できます。一方、1 ページあたりの 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() を呼び出すことができます。他の mip レベルはすべて、WebGL によって生成されます。

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

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

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 をご参照ください。