WebGPU 中的緩衝映射機制

1. 什麼是緩衝映射

就不給定義了,直接簡單的說,映射(Mapping)後的某塊顯存,就能被 CPU 訪問。

三大圖形 API(D3D12、Vulkan、Metal)的 Buffer(指顯存)映射後,CPU 就能訪問它了,此時注意,GPU 仍然可以訪問這塊顯存。這就會導致一個問題:IO衝突,這就需要程序考量這個問題了。

WebGPU 禁止了這個行爲,改用傳遞“所有權”來表示映射後的狀態,頗具 Rust 的哲學。每一個時刻,CPU 和 GPU 是單邊訪問顯存的,也就避免了競爭和衝突。

當 JavaScript 請求映射顯存時,所有權並不是馬上就能移交給 CPU 的,GPU 這個時候可能手頭上還有別的處理顯存的操作。所以,GPUBuffer 的映射方法是一個異步方法:

const someBuffer = device.createBuffer({ /* ... */ })
await someBuffer.mapAsync(GPUMapMode.READ, 0, 4) // 從 0 開始,只映射 4 個字節

// 之後就可以使用 getMappedRange 方法獲取其對應的 ArrayBuffer 進行緩衝操作

不過,解映射操作倒是一個同步操作,CPU 用完後就可以解映射:

somebuffer.unmap()

注意,mapAsync 方法將會直接在 WebGPU 內部往設備的默認隊列中壓入一個操作,此方法作用於 WebGPU 中三大時間軸中的 隊列時間軸。而且在 mapAsync 成功後,內存纔會增加(實測)。

當向隊列提交指令緩衝後(此指令緩衝的某個渲染通道要用到這塊 GPUBuffer),內存上的數據纔會提交給 GPU(猜測)。

由於測試地不多,我在調用 destroy 方法後並未顯著看到內存的變少,希望有朋友能測試。

創建時映射

可以在創建緩衝時傳遞 mappedAtCreation: true,這樣甚至都不需要聲明其 usage 帶有 GPUBufferUsage.MAP_WRITE

const buffer = device.createBuffer({
  usage: GPUBufferUsage.UNIFORM,
  size: 256,
  mappedAtCreation: true,
})
// 然後馬上就可以獲取映射後的 ArrayBuffer
const mappedArrayBuffer = buffer.getMappedRange()

/* 在這裏執行一些寫入操作 */

// 解映射,還管理權給 GPU
buffer.unmap()

2 緩衝數據的流向

2.1 CPU 至 GPU

JavaScript 這端會在 rAF 中頻繁地將大量數據傳遞給 GPUBuffer 映射出來的 ArrayBuffer,然後隨着解映射、提交指令緩衝到隊列,最後傳遞給 GPU.

上述最常見的例子莫過於傳遞每一幀所需的 VertexBuffer、UniformBuffer 以及計算通道所需的 StorageBuffer 等。

使用隊列對象的 writeBuffer 方法寫入緩衝對象是非常高效率的,但是與用來寫入的映射後的一個 GPUBuffer 相比,writeBuffer 有一個額外的拷貝操作。推測會影響性能,雖然官方推薦的例子中有很多 writeBuffer 的操作,大多數是用於 UniformBuffer 的更新。

2.2 GPU 至 CPU

這樣反向的傳遞比較少,但也不是沒有。譬如屏幕截圖(保存顏色附件到 ArrayBuffer)、計算通道的結果統計等,就需要從 GPU 的計算結果中獲取數據。

譬如,官方給的從渲染的紋理中獲取像素數據例子:

const texture = getTheRenderedTexture()

const readbackBuffer = device.createBuffer({
  usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
  size: 4 * textureWidth * textureHeight,
})

// 使用指令編碼器將紋理拷貝到 GPUBuffer
const encoder = device.createCommandEncoder()
encoder.copyTextureToBuffer(
  { texture },
  { buffer, rowPitch: textureWidth * 4 },
  [textureWidth, textureHeight],
)
device.submit([encoder.finish()])

// 映射,令 CPU 端的內存可以訪問到數據
await buffer.mapAsync(GPUMapMode.READ)
// 保存屏幕截圖
saveScreenshot(buffer.getMappedRange())
// 解映射
buffer.unmap()
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章