從 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)。雖然兩種語言不同,但基礎概念大致相同。

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

全球狀態

WebGL 有許多全域狀態。部分設定會套用至所有算繪作業,例如綁定哪些紋理和緩衝區。您可以透過呼叫各種 API 函式來設定這個全域狀態,且在您變更之前,該狀態會持續有效。WebGL 中的全域狀態是主要錯誤來源,因為很容易忘記變更全域設定。此外,全域狀態會導致程式碼共用作業變得困難,因為開發人員必須小心謹慎,避免不小心變更全域狀態,進而影響程式碼的其他部分。

WebGPU 是無狀態 API,不會維護全域狀態。而是使用管道的概念,封裝 WebGL 中全域的所有轉譯狀態。管道包含要使用的混合、拓樸和屬性等資訊。管道無法變更。如要變更部分設定,您必須建立另一個管道。WebGPU 也會使用指令編碼器將指令分批處理,並依照記錄的順序執行。這在陰影對應中很實用,例如在單一物件傳遞中,應用程式可以為每個光源陰影地圖記錄多個指令串流。

總而言之,由於 WebGL 的全球狀態模型會使建立可組合且穩定的程式庫和應用程式變得困難且不穩定,WebGPU 大幅減少了開發人員在向 GPU 傳送指令時需要追蹤的狀態數量。

不再同步

在 GPU 上,同步傳送指令並等待指令執行通常效率不彰,因為這可能會刷新管道並導致氣泡。這點在 WebGPU 和 WebGL 中尤其適用,因為這兩者採用多程序架構,且 GPU 驅動程式會在與 JavaScript 分開的程序中執行。

舉例來說,在 WebGL 中,呼叫 gl.getError() 需要從 JavaScript 程序到 GPU 程序,再從 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,但該 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 也允許您為每個 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,也可以瞭解如何自行產生 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 軸方向會沿著 framebuffer 座標、可視區域座標和片段/像素座標向下縮減。在剪輯空間中,Y 軸方向仍與 WebGL 相同。

特別銘謝

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

另外,我還建議您參閱 WebGPUFundamentals.org,深入瞭解 WebGPU 和 WebGL 之間的差異。