WebGL から WebGPU へ

François Beaufort
François Beaufort

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

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

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

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

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

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

頂点シェーダーやフラグメント シェーダーとは異なり、このシェーダーはグラフィック処理に限定されず、ML、物理シミュレーション、科学技術計算などの幅広いタスクに使用できます。コンピューティング シェーダーは、数百、場合によっては数千のスレッドで並列実行されるため、大規模なデータセットを効率的に処理できます。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、 keepDrawingBuffer、stencil などのコンテキスト属性を指定すると、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() を呼び出せます。WebGL により、他のすべての mip レベルが生成されます。

WebGPU では、mipmap をご自身で生成する必要があります。これを行う組み込み関数はありません。この決定について詳しくは、仕様に関する説明をご覧ください。webgpu-utils などの便利なライブラリを使用して 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 軸を画面の外側に配置するメタル規則を使用します。なお、フレームバッファ座標、ビューポート座標、フラグメント/ピクセル座標において、Y 軸の方向は下になります。クリップ空間では、WebGL と同様、Y 軸の方向は上向きです。

謝辞

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

また、WebGPU と WebGL の違いについて詳しくは、WebGPUFundamentals.org もおすすめします。