1. 核心概念
這部分不會詳細展開,以後寫教程時會深入。以下只是核心概念,是絕大多數 WebGPU 原生程序要接觸的,並不是全部。
① 適配器和設備
適配器,也就是 GPUAdapter
,指代真正的物理顯卡,WebGPU 給了個對象來代替它:
const adapter = await navigator.gpu.requestAdapter()
它提供了一個最重要行爲,請求設備對象 GPUDevice
:
const device = await adapter.requestDevice()
那麼什麼是 Device?其實,顯卡很忙。
WebGPU 程序只是三大圖形 API 中某個的“上層封裝”,除了 WebGPU,調用三大圖形 API 的程序遠不止,遊戲、三維建模工具、視頻編解碼器,都有可能會調用,甚至會直接調取 GPU 廠商給的 SDK 或驅動程序。
顯然,作爲顯卡“本身”,適配器爲了極高效率地工作,餵給它的數據資源和指令最好就是翻譯過的,儘可能專注地執行計算 —— 就像大老闆不可能日理萬機一樣,最好給到老闆的決策資料,就是經過整理的,他要做的就是使用他多年的經驗快速決策、簽字(效率高的老總 = RTX4090,超市小老闆 = GT1030)。
那麼,誰負責與各個部門(各個對顯卡有需要的程序)負責人溝通具體業務呢?
我認爲是老總的全權代理人,一般是祕書 + 副總經理。
不同封裝有不同的概念,至少在 WebGPU 中,這個代理人叫做“設備”,GPUDevice
,它幾乎就是顯卡的分身,WebGPU 程序中所要調取的資源、創建的對象、要觸發的行爲,都交給設備對象實現。
每個 WebGPU 程序應該都有自己的 GPUDevice
,不同的設備對象創建的 Buffer、Texture 等資源是不互通的,而適配器呢,一般情況下是同一個,除非你短時間內把電腦的顯卡給更改過,前一會兒是獨顯,過一會兒可能是核顯了(這段話還有待技術驗證,僅爲我不負責任的猜測)。
如果你寫過原生的 WebGL,你可能會聯想到 gl 上下文變量了,沒錯,設備對象大部分時候就是 gl 上下文的作用,但是是有本質區別的。
② 緩衝、紋理、採樣器
緩衝、紋理,即 GPUBuffer
、GPUTexture
均是 GPU 顯存中的數據對象,能在客戶端代碼(如果沒特別說明,就是指瀏覽器端的 JavaScript)組織、創建、上載數據、相互轉化、反讀數據。
WebGPU 進行渲染繪圖時,Canvas 是一個特殊的 GPUTexture
。
採樣器則是着色器程序對紋理採樣時的參數封裝。
看起來是 WebGL 類似對象 WebGLBuffer
、WebGLTexture
以及紋理採樣函數的“升級”,實際上調用時提供了更細緻的傳參,在數據上載、紋理與緩衝相互轉化、再從顯存讀取到內存的“映射機制”上卻大有不同。
這三個對象被稱作“資源”,均由 GPUDevice
創建。
③ 綁定組
綁定組,我更願意稱之爲“資源綁定組”,即 GPUBindGroup
;資源即“緩衝、紋理、採樣器”的任意組合。
使用綁定組,允許把一組你需要的資源“打組”,傳進着色器代碼中,它與下面的“管線”是緊密相關的。
爲什麼要打組呢?爲什麼我不能寫個函數,按我需要把 GPUBuffer
、GPUTexture
、GPUSampler
挨個像 WebGL 一樣綁定到某個綁定點呢?
有兩個原因:
- 性能角度:打組本身就是減少 CPU 到 GPU 信號通訊的一種方式,想想你的硬盤,是連續大文件傳得快,還是細碎的小文件快?
- 複用角度:不同的着色行爲可能會用一樣的資源集合,此時同一個綁定組就可以複用;想一想,肉餡兒塞進包子裏叫肉包,包進餃子皮裏就是肉餃子了。
綁定組是由 GPUDevice
創建的,是由第 ⑤ 小節中的 可編程通道編碼器 調用並與管線實際一起運作的。
④ 着色器與管線
着色器即 GPUShaderModule
,管線一般指 GPURenderPipeline
、GPUComputePipeline
兩個。
着色器支持把任意着色器混在一段字符串中,頂點着色器、片元着色器、計算着色器可以共用一個 GPUShaderModule
對象,只需指定入口函數,這點與 WebGL 分開創建 VS、FS 是不一樣的。
管線可不是 WebGLProgram
的升級,雖然 gl.useProgram
和 passEncoder.setPipeline
在行爲上有類似的作用,即切換到指定的行爲過程,但是,在 WebGPU 中這兩個管線對象,除了附着對應的着色器對象外,還限定着管線不同階段對應的狀態參數。有三個狀態參數對應着兩大管線:
-
vertex、fragment
-
compute
例如:
/*
---------
這裏不詳細展開,僅作爲簡略
---------
*/
const positionAttribDesc: GPUVertexAttribute = {
shaderLocation: 0, // wgsl - @location(0)
offset: 0,
format: 'float32x3'
}
const colorAttribDesc: GPUVertexAttribute = {
shaderLocation: 1, // wgsl - @location(1)
offset: 0,
format: 'float32x3'
}
const positionBufferDesc: GPUVertexBufferLayout = {
attributes: [positionAttribDesc],
arrayStride: 4 * 3, // sizeof(float) * 3
}
const colorBufferDesc: GPUVertexBufferLayout = {
attributes: [colorAttribDesc],
arrayStride: 4 * 3, // sizeof(float) * 3
}
// --- 創建 state 參數對象
const vertexState: GPUVertexState = {
module: shaderModule,
entryPoint: 'vs_main',
buffers: [positionBufferDesc, colorBufferDesc]
}
const fragmentState: GPUFragmentState = {
module: shaderModule,
entryPoint: 'fs_main',
targets: [{
format: navigator.gpu.getPreferredCanvasFormat()
}],
}
const primitiveState: GPUPrimitiveState = {
topology: 'triangle-list'
}
// --- 渲染管線 ---
const renderPipeline = device.createRenderPipeline({
layout: 'auto',
vertex: vertexState,
fragment: fragmentState,
primitive: primitiveState
})
// --- 計算管線 ---
const computePipeline = device.createComputePipeline({
layout: 'auto',
compute: {
module: shaderModule,
entryPoint: 'cs_main',
}
})
對應 GPUVertexState
、GPUFragmentState
、GPUComputeState
類型;上面說到綁定組是與管線緊密相關的,這幾個狀態參數對象,與綁定組中的各個資源對象有着對應關係。
着色器模塊對象和管線對象也是由 GPUDevice
創建的,管線對象甚至提供了異步創建的方法。
⑤ 編碼器與隊列
WebGPU 使用“編碼器”去“記錄”一幀內要做什麼事情,譬如切換管線、設定接下來要用什麼緩衝、綁定組,進而要進行什麼操作(繪圖或觸發並行計算)。
這有什麼好處?
編碼器“記錄”這些行爲,是在 CPU 側,也就是 JavaScript 完成的,這就解決了 WebGL 全局狀態對象的問題:改變一個狀態,就要發起一條或多條 GL 函數的調用(儘管使用擴展或在 WebGL 2.0 用各種技術進行了彌補,但是也不能實際解決問題)。
編碼記錄完成後,會在 CPU 這邊生成一個叫做“指令緩衝”對象,把當前幀的所有指令緩衝一次性提交給一個隊列,那麼當前幀就結束了戰鬥。
合情合理,大部分的邏輯組織交給更擅長處理這些事情的 CPU 完成,最後集中發射給 GPU,這就是 WebGPU 於 WebGL 的一大優點。
編碼器有哪些?
上面一段文字比較粗略。
首先,爲了區分繪圖操作、GPU 通用計算操作,WebGPU 使用“渲染通道編碼器”、“計算通道編碼器”,也就是 GPURenderPassEncoder
、GPUComputePassEncoder
來實現各自的行爲編碼、記錄;以渲染通道編碼爲例:
上圖參考自博客 Raw WebGPU。
而能創建這兩個特定 GPU 計算的“通道編碼器”的,叫做“指令編碼器”,也就是 GPUCommandEncoder
:
指令編碼器除了承載上面兩個通道編碼器的編碼結果外,還額外提供了資源的拷貝行爲、查詢行爲的編碼,例如紋理與緩衝對象之間的互相拷貝等:
在實際的代碼中,是按 GPUCommandEncoder
調用某個方法的順序進行記錄的,例如 beginRenderPass()
、copyBufferToTexture()
等。
隊列與指令緩衝
指令編碼器的 finish
方法返回一個指令緩衝對象,即 GPUCommandBuffer
,這個可以提交給隊列對象 GPUQueue
,隊列對象是設備對象上的一個實例字段。
排列在隊列上的除了指令緩衝,還有隊列自己發出的“隊列時間線”上的行爲,例如寫入緩衝數據、寫入紋理數據等。圖示如下:
2. 重要機制
① 緩衝映射機制
緩衝映射,簡單的說就是使得內存、顯存中的緩衝數據可以交換着用的一種機制。詳細的文章可以參考:
② 時間線
WebGPU 規範中不同的行爲也許發生在的層面是不一樣的,每個層面在運作的過程中都有它自己的時間線。規範給出了三條時間線:
-
內容時間線:內容時間線上的行爲,大多數是 JavaScript 對象的創建、JavaScript 方法的調用,這是最上面的一層;
-
設備時間線:此“設備”非
GPUDevice
;設備時間線上的行爲,大多數是指瀏覽器底層 WebGPU 實現中的變化,這類行爲的層級低於 JavaScript 的執行,操作的是“內部對象”,卻還沒到 GPU 執行的部分,例如生成指令緩衝; -
隊列時間線:此“隊列”非
GPUQueue
;隊列時間線上發生的行爲,通常就是指 GPU 中具體任務的執行,例如繪製、資源上載、資源複製、通用計算調度等。