WebGPU 導入[2] - 核心概念與重要機制解讀


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 程序中所要調取的資源、創建的對象、要觸發的行爲,都交給設備對象實現。

image

每個 WebGPU 程序應該都有自己的 GPUDevice,不同的設備對象創建的 Buffer、Texture 等資源是不互通的,而適配器呢,一般情況下是同一個,除非你短時間內把電腦的顯卡給更改過,前一會兒是獨顯,過一會兒可能是核顯了(這段話還有待技術驗證,僅爲我不負責任的猜測)。

如果你寫過原生的 WebGL,你可能會聯想到 gl 上下文變量了,沒錯,設備對象大部分時候就是 gl 上下文的作用,但是是有本質區別的。

② 緩衝、紋理、採樣器

緩衝、紋理,即 GPUBufferGPUTexture 均是 GPU 顯存中的數據對象,能在客戶端代碼(如果沒特別說明,就是指瀏覽器端的 JavaScript)組織、創建、上載數據、相互轉化、反讀數據。

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

採樣器則是着色器程序對紋理採樣時的參數封裝。

看起來是 WebGL 類似對象 WebGLBufferWebGLTexture 以及紋理採樣函數的“升級”,實際上調用時提供了更細緻的傳參,在數據上載、紋理與緩衝相互轉化、再從顯存讀取到內存的“映射機制”上卻大有不同。

這三個對象被稱作“資源”,均由 GPUDevice 創建。

③ 綁定組

綁定組,我更願意稱之爲“資源綁定組”,即 GPUBindGroup;資源即“緩衝、紋理、採樣器”的任意組合。

使用綁定組,允許把一組你需要的資源“打組”,傳進着色器代碼中,它與下面的“管線”是緊密相關的。

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

有兩個原因:

  • 性能角度:打組本身就是減少 CPU 到 GPU 信號通訊的一種方式,想想你的硬盤,是連續大文件傳得快,還是細碎的小文件快?
  • 複用角度:不同的着色行爲可能會用一樣的資源集合,此時同一個綁定組就可以複用;想一想,肉餡兒塞進包子裏叫肉包,包進餃子皮裏就是肉餃子了。

image

綁定組是由 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 來實現各自的行爲編碼、記錄;以渲染通道編碼爲例:

image

上圖參考自博客 Raw WebGPU

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

image

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

image

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

隊列與指令緩衝

指令編碼器的 finish 方法返回一個指令緩衝對象,即 GPUCommandBuffer,這個可以提交給隊列對象 GPUQueue,隊列對象是設備對象上的一個實例字段。

排列在隊列上的除了指令緩衝,還有隊列自己發出的“隊列時間線”上的行爲,例如寫入緩衝數據、寫入紋理數據等。圖示如下:

image

2. 重要機制

① 緩衝映射機制

緩衝映射,簡單的說就是使得內存、顯存中的緩衝數據可以交換着用的一種機制。詳細的文章可以參考:

# WebGPU 中的緩衝映射機制

② 時間線

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

  • 內容時間線:內容時間線上的行爲,大多數是 JavaScript 對象的創建、JavaScript 方法的調用,這是最上面的一層;

  • 設備時間線:此“設備”非 GPUDevice;設備時間線上的行爲,大多數是指瀏覽器底層 WebGPU 實現中的變化,這類行爲的層級低於 JavaScript 的執行,操作的是“內部對象”,卻還沒到 GPU 執行的部分,例如生成指令緩衝;

  • 隊列時間線:此“隊列”非 GPUQueue;隊列時間線上發生的行爲,通常就是指 GPU 中具體任務的執行,例如繪製、資源上載、資源複製、通用計算調度等。

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