這部分不會詳細展開,以後寫教學時會深入。以下只是核心概念,是絕大多數 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 一樣繫結到某個繫結點呢?
有兩個原因:
繫結組是由 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
,佇列物件是裝置物件上的一個範例欄位。
排列在佇列上的除了指令緩衝,還有佇列自己發出的「佇列時間線」上的行為,例如寫入緩衝資料、寫入紋理資料等。圖示如下:
緩衝對映,簡單的說就是使得記憶體、視訊記憶體中的緩衝資料可以交換著用的一種機制。詳細的文章可以參考:
WebGPU 規範中不同的行為也許發生在的層面是不一樣的,每個層面在運作的過程中都有它自己的時間線。規範給出了三條時間線:
內容時間線:內容時間線上的行為,大多數是 JavaScript 物件的建立、JavaScript 方法的呼叫,這是最上面的一層;
裝置時間線:此「裝置」非 GPUDevice
;裝置時間線上的行為,大多數是指瀏覽器底層 WebGPU 實現中的變化,這類行為的層級低於 JavaScript 的執行,操作的是「內部物件」,卻還沒到 GPU 執行的部分,例如生成指令緩衝;
佇列時間線:此「佇列」非 GPUQueue
;佇列時間線上發生的行為,通常就是指 GPU 中具體任務的執行,例如繪製、資源上載、資源複製、通用計算排程等。