從 WebGL 到 WebGPU

François Beaufort
François Beaufort

身為 WebGL 開發人員,使用 WebGPU 後,或許感到興奮不已,而 WebGL 正是 WebGL 的後續版本,可將現代圖形 API 推向網路。

WebGL 和 WebGPU 有許多共通的核心概念,令人安心。這兩個 API 都可讓您在 GPU 上執行名為著色器的小型程式。WebGL 支援頂點和片段著色器,而 WebGPU 也支援運算著色器。WebGL 採用 OpenGL 著色語言 (GLSL),WebGPU 則使用 WebGPU 著色語言 (WGSL)。雖然這兩種語言不同,但基礎概念大致相同。

因此,本文重點說明 WebGL 和 WebGPU 的一些差異,協助您快速上手。

全球狀態

WebGL 的全球性狀態很多。部分設定適用於所有算繪作業,例如已繫結的紋理和緩衝區。透過呼叫各種 API 函式來設定這個全域狀態,此設定會保持有效,直到您進行變更為止。WebGL 中的全域狀態是重大錯誤來源,因為很容易忘記變更全域設定。此外,全域狀態會導致程式碼共用困難,因為開發人員需要小心,不得以會影響程式碼其他部分的方式意外變更全域狀態。

WebGPU 是無狀態 API,不會維持全域狀態。而是使用管道的概念來封裝 WebGL 中全域的所有轉譯狀態。管道包含要使用的混合、拓撲和屬性等資訊。管道無法變更。如要變更某些設定,則需建立另一個管道。WebGPU 也使用指令編碼器將指令批次處理,並依照記錄順序執行這些指令。這在陰影對應中非常有用,舉例來說,在對物件的單次傳遞中,應用程式可以記錄多個指令串流,每個指令串流用於每個光源的陰影對應。

總而言之,WebGL 的全球狀態模型使得建立強大且可組合的程式庫和應用程式相當困難與脆弱,WebGPU 大幅降低了開發人員傳送指令至 GPU 時需要追蹤的狀態量。

不要再同步處理

在 GPU 上,傳送指令並同步等待通常效率不彰,因為這可能會清除管道並導致對話框。在 WebGPU 和 WebGL 中尤其如此,因為採用多程序架構,且 GPU 驅動程式是透過獨立程序而非 JavaScript 執行。

舉例來說,在 WebGL 中呼叫 gl.getError() 時,需要從 JavaScript 程序到 GPU 程序和返回位置的同步 IPC。這會導致 CPU 端出現說明框,因為兩個程序的通訊。

為避免出現這類泡泡,WebGPU 的設計宗旨是完全非同步錯誤模型和所有其他作業皆以非同步的方式進行。例如,當您建立紋理時,即使紋理實際上有誤,作業也會立即成功顯示。您只能以非同步方式找出錯誤。這項設計可確保跨程序的通訊順暢,並為應用程式提供可靠的效能。

運算著色器

運算著色器是在 GPU 上執行的程式,可執行一般用途運算。這些格式僅適用於 WebGPU,不支援 WebGL。

有別於頂點和片段著色器,這類著色器不限於圖形處理,可以應用於多種工作,例如機器學習、物理模擬和科學運算。運算著色器由數百或數千個執行緒平行執行,因此處理大型資料集的效率極高。進一步瞭解 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、antialias、colorSpace、depth、keepDrawingBuffer 或模板) 後,WebGL 就會自動管理畫布。

另一方面,WebGPU 則要求您自行管理畫布。舉例來說,如要在 WebGPU 中達到反鋸齒,您可以建立多重取樣紋理並予以算繪。接著,您會將 Multisample 紋理解析為一般紋理,並將該紋理繪製到畫布上。透過這項手動管理功能,您可以從單一 GPUDevice 物件輸出所需畫布,數量不限。相反地,WebGL 在每個畫布上只能建立一種背景內容。

請參考 WebGPU 多畫布示範

此外,瀏覽器目前設有每頁 WebGL 畫布數量的限制。在本文撰寫期間,Chrome 和 Safari 最多只能同時使用 16 個 WebGL 畫布;Firefox 最多可建立 200 條格式。另一方面,每頁 WebGPU 畫布數量沒有限制。

螢幕截圖:在 Safari、Chrome 和 Firefox 瀏覽器中,可選擇的 WebGL 畫布數量上限
Safari、Chrome 和 Firefox (從左到右) WebGL 畫布的數量上限 - 示範

實用的錯誤訊息

WebGPU 可為 API 傳回的每則訊息提供呼叫堆疊。這表示您可以快速查看程式碼中發生錯誤的位置,這些資訊有助於偵錯及修正錯誤

除了提供呼叫堆疊之外,WebGPU 錯誤訊息也很容易理解並可供採取行動。錯誤訊息通常會附上錯誤說明和錯誤修正建議。

您也可以透過 WebGPU 為每個 WebGPU 物件提供自訂 label。瀏覽器稍後會在 GPU 錯誤訊息、控制台警告和瀏覽器開發人員工具中使用這個標籤。

從名稱到索引

WebGL 中的許多物件都是透過名稱連結。舉例來說,您可以在 GLSL 中宣告名為 myUniform 的統一變數,並使用 gl.getUniformLocation(program, 'myUniform') 取得其位置。如果您打錯統一變數的名稱,這是相當實用的做法。

另一方面,在 WebGPU 中,所有項目都是透過位元組偏移或索引建立完全連結 (通常稱為「位置」)。您有責任讓 WGSL 和 JavaScript 中的程式碼位置保持同步。

產生 mipmap

在 WebGL 中,您可以建立紋理的 0 mip 層級,然後呼叫 gl.generateMipmap()。WebGL 隨即會為您產生其他 mip 層級。

在 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 的 clip 空間範圍介於 -1 到 1 之間。在 WebGPU 中,Z 的裁剪空間範圍是 0 到 1。這表示 z 值為 0 的物件與相機最接近,z 值為 1 的物件則最遠。

插圖:WebGL 和 WebGPU 的 Z 裁剪空間範圍。
WebGL 和 WebGPU 中的 Z 裁剪空間範圍。

WebGL 採用 OpenGL 慣例,也就是 Y 軸向上,Z 軸指向檢視器。WebGPU 採用金屬慣例,也就是 Y 軸向下,Z 軸指向螢幕外。請注意,Y 軸方向會沿著 framebuffer 座標、可視區域座標和片段/像素座標向下縮減。在剪輯空間中,Y 軸方向仍與 WebGL 相同。

特別銘謝

感謝 Corentin Wallez、Greg Tavares、Stephen White、Ken Russell 和 Rachel Andrew 審閱這篇文章,

我也會建議您使用 WebGPUFundamentals.org 深入瞭解 WebGPU 和 WebGL 之間的差異。