WebGL 與 WebGPU 比對[1] 前奏


這篇講講歷史,不太適合直奔主題的朋友們。

1 爲什麼是 WebGPU 而不是 WebGL 3.0

你若往 Web 圖形技術的底層去深究,一定能追溯到上個世紀 90 年代提出的 OpenGL 技術,也一定能看到,WebGL 就是基於 OpenGL ES 做出來的這些信息。OpenGL 在那個顯卡羸弱的年代發揮了它應有的價值。

顯卡驅動

我們都知道現在的顯卡都要安裝顯卡驅動程序,通過顯卡驅動程序暴露的 API,我們就可以操作 GPU 完成圖形處理器的操作。

問題就是,顯卡驅動和普通編程界的彙編一樣,底層,不好寫,於是各大廠就做了封裝 —— 碼界的基操。

圖形 API 的簡單年表

OpenGL 就是幹這個的,負責上層接口封裝並與下層顯卡驅動打交道,但是,衆所周知,它的設計風格已經跟不上現代 GPU 的特性了。

Microsoft 爲此做出來最新的圖形API 是 Direct3D 12,Apple 爲此做出來最新的圖形API 是 Metal,有一個著名的組織則做出來 Vulkan,這個組織名叫 Khronos。D3D12 現在在發光發熱的地方是 Windows 和 PlayStation,Metal 則是 Mac 和 iPhone,Vulkan 你可能在安卓手機評測上見得多。這三個圖形 API 被稱作三大現代圖形API,與現代顯卡(無論是PC還是移動設備)的聯繫很密切。

WebGL 能運行在各個瀏覽器的原因

噢,忘了一提,OpenGL 在 2006 年把丟給了 Khronos 管,現在各個操作系統基本都沒怎麼裝這個很老的圖形驅動了。

那問題來了,基於 OpenGL ES 的 WebGL 爲什麼能跑在各個操作系統的瀏覽器?

因爲 WebGL 再往下已經可以不是 OpenGL ES 了,在 Windows 上現在是通過 D3D 轉譯到顯卡驅動的,在 macOS 則是 Metal,只不過時間越接近現在,這種非親兒子式的實現就越發困難。

蘋果的 Safari 瀏覽器最近幾年才珊珊支持 WebGL 2.0,而且已經放棄了 OpenGL ES 中 GPGPU 的特性了,或許看不到 WebGL 2.0 的 GPGPU 在 Safari 上實現了,果子哥現在正忙着 Metal 和更偉大的 M 系列自研芯片呢。

WebGPU 的名稱由來

所以,綜上所述,下一代的 Web 圖形接口不叫 WebGL 3.0 的原因,你清楚了嗎?已經不是 GL 一脈的了,爲了使現代巨頭在名稱上不打架,所以採用了更貼近硬件名稱的 WebGPU,WebGPU 從根源上和 WebGL 就不是一個時代的,無論是編碼風格還是性能表現上。

題外話,OpenGL 並不是沒有學習的價值,反而它還會存在一段時間,WebGL 也一樣。

2 與 WebGL 比較編碼風格

WebGL 實際上可以說是 OpenGL 的影子,OpenGL 的風格對 WebGL 的風格影響巨大。

學習過 WebGL 接口的朋友都知道一個東西:gl 變量,準確的說是 WebGLRenderingContext 對象,WebGL 2.0 則是 WebGLRenderingContext2.

OpenGL 的編碼風格

無論是操作着色器,還是操作 VBO,亦或者是創建一些 Buffer、Texture 對象,基本上都得通過 gl 變量一條一條函數地走過程,順序是非常講究的,例如,下面是創建兩大着色器並執行編譯、連接的代碼:

const vertexShaderCode = `
attribute vec4 a_position;
void main() {
	gl_Position = a_position;
}
`

const fragmentShaderCode = `
precision mediump float;
void main() {
  gl_FragColor = vec4(1, 0, 0.5, 1);
}
`

const vertexShader = gl.createShader(gl.VERTEX_SHADER)
gl.shaderSource(vertexShader, vertexShaderCode)
gl.compileShader(vertexShader)
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER)
gl.shaderSource(fragmentShader, fragmentShaderCode)
gl.compileShader(fragmentShader)

const program = gl.createProgram()
gl.attachShader(program, vertexShader)
gl.attachShader(program, fragmentShader)
gl.linkProgram(program)

// 還需要顯式指定你需要用哪個 program
gl.useProgram(program)
// 繼續操作頂點數據並觸發繪製
// ...

創建着色器、賦予着色器代碼並編譯的三行 js WebGL 調用,可以說是必須這麼寫了,頂多 vs 和 fs 的創建編譯順序可以換一下,但是又必須在 program 之前完成這些操作。

CPU 負載問題

有人說這無所謂,可以封裝成 JavaScript 函數,隱藏這些過程細節,只需傳遞參數即可。是,這是一個不錯的封裝,很多 js 庫都做過,並且都很實用。

但是,這仍然有難以逾越的鴻溝 —— 那就是 OpenGL 本身的問題。

每一次調用 gl.xxx 時,都會完成 CPU 到 GPU 的信號傳遞,改變 GPU 的狀態,是立即生效的。熟悉計算機基礎的朋友應該知道,計算機內部的時間和硬件之間的距離有多麼重要,世人花了幾十年時間無不爲信號傳遞付出了努力,上述任意一條 gl 函數改變 GPU 狀態的過程,大致要走完 CPU ~ 總線 ~ GPU 這麼長一段距離。

我們都知道,辦事肯定是一次性備齊材料的好,不要來來回回跑那麼多遍,而 OpenGL 就是這樣子的。有人說爲什麼要這樣而不是改成一次發送的樣子?歷史原因,OpenGL 盛行那會兒 GPU 的工作沒那麼複雜,也就不需要那麼超前的設計。

綜上所述,WebGL 是存在 CPU 負載隱患的,是由於 OpenGL 這個狀態機制決定的。

現代三大圖形API 可不是這樣,它們更傾向於先把東西準備好,最後提交給 GPU 的就是一個完整的設計圖紙和緩衝數據,GPU 只需要拿着就可以專注辦事。

WebGPU 的裝配式編碼風格

WebGPU 雖然也有一個總管家一樣的對象 —— device,類型是 GPUDevice,表示可以操作 GPU 設備的一個高層級抽象,它負責創建操作圖形運算的各個對象,最後裝配成一個叫 “CommandBuffer(指令緩衝,GPUCommandBuffer)”的對象並提交給隊列,這才完成 CPU 這邊的勞動。

所以,device.createXXX 創建過程中的對象時,並不會像 WebGL 一樣立即通知 GPU 完成狀態的改變,而是在 CPU 端寫的代碼就從邏輯、類型上確保了待會傳遞給 GPU 的東西是準確的,並讓他們按自己的坑站好位,隨時等待提交給 GPU。

在這裏,指令緩衝對象具備了完整的數據資料(幾何、紋理、着色器、管線調度邏輯等),GPU 一拿到就知道該幹什麼。

// 在異步函數中
const device = await adapter.requestDevice()
const buffer = device.createBuffer({
  /* 裝配幾何,傳遞內存中的數據,最終成爲 vertexAttribute 和 uniform 等資源 */
})
const texture = device.createTexture({
  /* 裝配紋理和採樣信息 */
})

const pipelineLayout = device.createPipelineLayout({
  /* 創建管線佈局,傳遞綁定組佈局對象 */
})

/* 創建着色器模塊 */
const vertexShaderModule = device.createShaderModule({ /* ... */ })
const fragmentShaderModule = device.createShaderModule({ /* ... */ })

/*
計算着色器可能用到的着色器模塊
const computeShaderModule = device.createShaderModule({ /* ... * / })
*/

const bindGroupLayout = device.createBindGroupLayout({
  /* 創建綁定組的佈局對象 */
})

const pipelineLayout = device.createPipelineLayout({
  /* 傳遞綁定組佈局對象 */
})

/*
上面兩個佈局對象其實可以偷懶不創建,綁定組雖然需要綁定組佈局以
通知對應管線階段綁定組的資源長啥樣,但是綁定組佈局是可以由
管線對象通過可編程階段的代碼自己推斷出來綁定組佈局對象的
本示例代碼保存了完整的過程
*/

const pipeline = device.createRenderPipeline({
  /* 
  創建管線
  指定管線各個階段所需的素材
  其中有三個階段可以傳遞着色器以實現可編程,即頂點、片段、計算 
  每個階段還可以指定其所需要的數據、信息,例如 buffer 等
  
  除此之外,管線還需要一個管線的佈局對象,其內置的綁定組佈局對象可以
  讓着色器知曉之後在通道中使用的綁定組資源是啥樣子的
  */
})

const bindGroup_0 = deivce.createBindGroup({
  /* 
  資源打組,將 buffer 和 texture 歸到邏輯上的分組中,
  方便各個過程調用,過程即管線,
  此處必須傳遞綁定組佈局對象,可以從管線中推斷獲取,也可以直接傳遞綁定組佈局對象本身
  */
})

const commandEncoder = device.createCommandEncoder() // 創建指令緩衝編碼器對象
const renderPassEncoder = commandEncoder.beginRenderPass() // 啓動一個渲染通道編碼器
// 也可以啓動一個計算通道
// const computePassEncoder = commandEncoder.beginComputePass({ /* ... */ }) 

/*
以渲染通道爲例,使用 renderPassEncoder 完成這個通道內要做什麼的順序設置,例如
*/

// 第一道繪製,設置管線0、綁定組0、綁定組1、vbo,並觸發繪製
renderPassEncoder.setPipeline(renderPipeline_0)
renderPassEncoder.setBindGroup(0, bindGroup_0)
renderPassEncoder.setBindGroup(1, bindGroup_1)
renderPassEncoder.setVertexBuffer(0, vbo, 0, size)
renderPassEncoder.draw(vertexCount)

// 第二道繪製,設置管線1、另一個綁定組並觸發繪製
renderPassEncoder.setPipeline(renderPipeline_1)
renderPassEncoder.setBindGroup(1, another_bindGroup)
renderPassEncoder.draw(vertexCount)

// 結束通道編碼
renderPassEncoder.endPass()

// 最後提交至 queue,也即 commandEncoder 調用 finish 完成編碼,返回一個指令緩衝
device.queue.submit([
  commandEncoder.finish()
])

上述過程是 WebGPU 的一般化代碼,很粗糙,沒什麼細節,不過基本上就是這麼個邏輯。

對通道編碼器的那部分代碼,筆者保留的比較完整,讓讀者更好觀察一個指令編碼器是如何編碼通道,並最後結束編碼創建指令緩衝提交給隊列的。

廚子戲法

用做菜來比喻,OpenGL 系的編程就好比做一道菜時需要什麼調料就去拿什麼調料,做好一道菜再繼續做下一道菜;而現代圖形API 則是多竈臺開火,所有材料都在合適的位置上,包括處理好的食材和輔料,即使一個廚師(CPU)都可以同時做好幾道菜,效率很高。

3 多線程與強大的通用計算(GPGPU)能力

WebWorker 多線程

WebGL 的總管家對象是 gl 變量,它必須依賴 HTML Canvas 元素,也就是說必須由主線程獲取,也只能在主線程調度 GPU 狀態,WebWorker 技術的多線程能力只能處理數據,比較雞肋。

WebGPU 改變了總管家對象的獲取方式,adapter 對象所依賴的 navigator.gpu 對象在 WebWorker 中也可以訪問,所以在 Worker 中也可以創建 device,也可以裝配出指令緩衝,從而實現多線程提交指令緩衝,實現 CPU 端多線程調度 GPU 的能力。

通用計算(GPGPU)

如果說 WebWorker 是 CPU 端的多線程,那麼 GPU 本身的多線程也要用上。

能實現這一點的,是一個叫做“計算着色器”的東西,它是可編程管線中的一個可編程階段,在 OpenGL 中可謂是姍姍來遲(因爲早期的顯卡並沒挖掘其並行通用計算的能力),更別說 WebGL 到了 2.0 才支持了,蘋果老兄甚至壓根就懶得給 WebGL 2.0 實現這個特性。

WebGPU 出廠就帶這玩意兒,通過計算着色器,使用 GPU 中 CU(Compute Unit,計算單元)旁邊的共享內存,速度比普通的顯存速度快得多。

有關計算着色器的資料不是特別多,目前只能看例子,在參考資料中也附帶了一篇博客。

將 GPGPU 帶入 Web 端後,腳本語言的運行時(deno、瀏覽器JavaScript,甚至未來的 nodejs 也有可能支持 WebGPU)就可以訪問 GPU 的強大並行計算能力,據說 tensorflow.js 改用 WebGPU 作爲後置技術後性能有極爲顯著的提升,對深度學習等領域有極大幫助,即使用戶的瀏覽器沒那麼新潮,渲染編程還沒那麼快換掉 WebGL,WebGPU 的通用計算能力也可以在別的領域發光發熱,更別說計算着色器在渲染中也是可以用的。

真是誘人啊!

4 瀏覽器的實現

Edge 和 Chrome 截至發文,在金絲雀版本均可以通過 flag 打開試用。

Edge 和 Chrome 均使用了 Chromium 核心,Chromium 是通過 Dawn 這個模塊實現的 WebGPU API,根據有關資料,Dawn 中的 DawnNative 部分負責與三大圖形 API 溝通,向上則給一個叫 DawnWire 的模塊傳遞信息,DawnWire 模塊則負責與 JavaScript API 溝通,也就是你寫的 WebGPU 代碼。WGSL 也是這個部分實現的。Dawn 是 C++ 實現的,你可以在參考資料中找到連接。

FireFox 則使用了 gfx-rs 項目實現 WebGPU,顯然是 Rust 語言實現的 WebGPU,也有與 Dawn 類似的模塊設計。

Safari 則更新自家的 WebKit 實現 WebGPU。

5 未來

展望宏圖之類的話不說,但是隨着紅綠藍三家的 GPU 技術越發精湛,加上各個移動端的 GPU 逐漸起色,三大現代圖形API肯定還在發展,WebGPU 一定能在 Web 端釋放現代圖形處理器(GPU)的強大能力,無論是圖形遊戲,亦或是通用並行計算帶來的機器學習、AI能力。

參考資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章