WebGPU 匯入[2]

2022-08-02 12:06:45


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 上下文的作用,但是是有本質區別的。

② 緩衝、紋理、取樣器

緩衝、紋理,即 GPUBufferGPUTexture 均是 GPU 視訊記憶體中的資料物件,能在使用者端程式碼(如果沒特別說明,就是指瀏覽器端的 JavaScript)組織、建立、上載資料、相互轉化、反讀資料。

WebGPU 進行渲染繪圖時,Canvas 是一個特殊的 GPUTexture

取樣器則是著色器程式對紋理取樣時的引數封裝。

看起來是 WebGL 類似物件 WebGLBufferWebGLTexture 以及紋理取樣函數的「升級」,實際上呼叫時提供了更細緻的傳參,在資料上載、紋理與緩衝相互轉化、再從視訊記憶體讀取到記憶體的「對映機制」上卻大有不同。

這三個物件被稱作「資源」,均由 GPUDevice 建立。

③ 繫結組

繫結組,我更願意稱之為「資源繫結組」,即 GPUBindGroup;資源即「緩衝、紋理、取樣器」的任意組合。

使用繫結組,允許把一組你需要的資源「打組」,傳進著色器程式碼中,它與下面的「管線」是緊密相關的。

為什麼要打組呢?為什麼我不能寫個函數,按我需要把 GPUBufferGPUTextureGPUSampler 挨個像 WebGL 一樣繫結到某個繫結點呢?

有兩個原因:

  • 效能角度:打組本身就是減少 CPU 到 GPU 訊號通訊的一種方式,想想你的硬碟,是連續大檔案傳得快,還是細碎的小檔案快?
  • 複用角度:不同的著色行為可能會用一樣的資源集合,此時同一個繫結組就可以複用;想一想,肉餡兒塞進包子裡叫肉包,包進餃子皮裡就是肉餃子了。

繫結組是由 GPUDevice 建立的,是由第 ⑤ 小節中的 可程式化通道編碼器 呼叫並與管線實際一起運作的。

④ 著色器與管線

著色器即 GPUShaderModule,管線一般指 GPURenderPipelineGPUComputePipeline 兩個。

著色器支援把任意著色器混在一段字串中,頂點著色器、片元著色器、計算著色器可以共用一個 GPUShaderModule 物件,只需指定入口函數,這點與 WebGL 分開建立 VS、FS 是不一樣的。

管線可不是 WebGLProgram 的升級,雖然 gl.useProgrampassEncoder.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',
  }
})

對應 GPUVertexStateGPUFragmentStateGPUComputeState 型別;上面說到繫結組是與管線緊密相關的,這幾個狀態引數物件,與繫結組中的各個資源物件有著對應關係。

著色器模組物件和管線物件也是由 GPUDevice 建立的,管線物件甚至提供了非同步建立的方法。

⑤ 編碼器與佇列

WebGPU 使用「編碼器」去「記錄」一幀內要做什麼事情,譬如切換管線、設定接下來要用什麼緩衝、繫結組,進而要進行什麼操作(繪圖或觸發平行計算)。

這有什麼好處?

編碼器「記錄」這些行為,是在 CPU 側,也就是 JavaScript 完成的,這就解決了 WebGL 全域性狀態物件的問題:改變一個狀態,就要發起一條或多條 GL 函數的呼叫(儘管使用擴充套件或在 WebGL 2.0 用各種技術進行了彌補,但是也不能實際解決問題)。

編碼記錄完成後,會在 CPU 這邊生成一個叫做「指令緩衝」物件,把當前幀的所有指令緩衝一次性提交給一個佇列,那麼當前幀就結束了戰鬥。

合情合理,大部分的邏輯組織交給更擅長處理這些事情的 CPU 完成,最後集中發射給 GPU,這就是 WebGPU 於 WebGL 的一大優點。

編碼器有哪些?

上面一段文字比較粗略。

首先,為了區分繪圖操作、GPU 通用計算操作,WebGPU 使用「渲染通道編碼器」、「計算通道編碼器」,也就是 GPURenderPassEncoderGPUComputePassEncoder 來實現各自的行為編碼、記錄;以渲染通道編碼為例:

上圖參考自部落格 Raw WebGPU

而能建立這兩個特定 GPU 計算的「通道編碼器」的,叫做「指令編碼器」,也就是 GPUCommandEncoder

指令編碼器除了承載上面兩個通道編碼器的編碼結果外,還額外提供了資源的拷貝行為、查詢行為的編碼,例如紋理與緩衝物件之間的互相拷貝等:

在實際的程式碼中,是按 GPUCommandEncoder 呼叫某個方法的順序進行記錄的,例如 beginRenderPass()copyBufferToTexture() 等。

佇列與指令緩衝

指令編碼器的 finish 方法返回一個指令緩衝物件,即 GPUCommandBuffer,這個可以提交給佇列物件 GPUQueue,佇列物件是裝置物件上的一個範例欄位。

排列在佇列上的除了指令緩衝,還有佇列自己發出的「佇列時間線」上的行為,例如寫入緩衝資料、寫入紋理資料等。圖示如下:

2. 重要機制

① 緩衝對映機制

緩衝對映,簡單的說就是使得記憶體、視訊記憶體中的緩衝資料可以交換著用的一種機制。詳細的文章可以參考:

# WebGPU 中的緩衝對映機制

② 時間線

WebGPU 規範中不同的行為也許發生在的層面是不一樣的,每個層面在運作的過程中都有它自己的時間線。規範給出了三條時間線:

  • 內容時間線:內容時間線上的行為,大多數是 JavaScript 物件的建立、JavaScript 方法的呼叫,這是最上面的一層;

  • 裝置時間線:此「裝置」非 GPUDevice;裝置時間線上的行為,大多數是指瀏覽器底層 WebGPU 實現中的變化,這類行為的層級低於 JavaScript 的執行,操作的是「內部物件」,卻還沒到 GPU 執行的部分,例如生成指令緩衝;

  • 佇列時間線:此「佇列」非 GPUQueue;佇列時間線上發生的行為,通常就是指 GPU 中具體任務的執行,例如繪製、資源上載、資源複製、通用計算排程等。