從 WebGL 到 WebGPU

François Beaufort
François Beaufort

身為 WebGL 開發人員,您可能既期待又怕受傷害,想開始使用 WebGPU (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、preserveDrawingBuffer 或 stencil) 後,WebGL 會自動管理畫布。

另一方面,WebGPU 則需要您自行管理畫布。舉例來說,如要在 WebGPU 中實現反鋸齒效果,您需要建立多重取樣紋理並將其算繪出來。接著,您會將多重取樣紋理解析為一般紋理,並將該紋理繪製到畫布上。透過這種手動管理方式,您可以從單一 GPUDevice 物件輸出至任意數量的畫布。相較之下,WebGL 每個畫布只能建立一個環境。

請參閱 WebGPU 多個畫布的示範

附帶一提,瀏覽器目前對每個網頁的 WebGL 畫布數量設有限制。撰寫本文時,Chrome 和 Safari 最多只能同時使用 16 個 WebGL 畫布,Firefox 則最多可建立 200 個。另一方面,每個網頁的 WebGPU 畫布數量沒有限制。

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

實用的錯誤訊息

WebGPU 會為 API 傳回的每則訊息提供呼叫堆疊。也就是說,您可以快速查看程式碼中發生錯誤的位置,這對偵錯和修正錯誤很有幫助

除了提供呼叫堆疊,WebGPU 錯誤訊息也容易瞭解且實用。錯誤訊息通常會說明錯誤,並提供修正建議。

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

從名稱到索引

在 WebGL 中,許多項目都是透過名稱連結。舉例來說,您可以在 GLSL 中宣告名為 myUniform 的統一變數,並使用 gl.getUniformLocation(program, 'myUniform') 取得其位置。如果輸入的制服變數名稱有誤,就會收到錯誤訊息,因此這項功能非常實用。

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

生成 Mipmap

在 WebGL 中,您可以建立紋理的第 0 層 Mip,然後呼叫 gl.generateMipmap()。WebGL 接著會為您產生所有其他 Mip 層級。

在 WebGPU 中,您必須自行產生 Mipmap。新 Pixel 裝置雖然沒有內建匯入記事的功能,如要進一步瞭解這項決策,請參閱規格討論。您可以使用 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 使用 Metal 慣例,Y 軸向下,Z 軸則超出螢幕。請注意,在 Framebuffer 座標、檢視區塊座標和片段/像素座標中,Y 軸方向為向下。在剪輯空間中,Y 軸方向仍與 WebGL 相同,都是向上。

特別銘謝

感謝 Corentin Wallez、Gregg Tavares、Stephen White、Ken Russell 和 Rachel Andrew 審查本文。

此外,我也建議您前往 WebGPUFundamentals.org,深入瞭解 WebGPU 和 WebGL 的差異。