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()