WebGL 和 Node.js 中都有 Buffer 的使用,簡單對比記錄一下兩個完全不相干的領域中 Buffer 異同,加強記憶。
Buffer 是用來儲存二進位制資料的「緩衝區」,其本身的定義和用途在任何技術領域都是一致的,跟 WebGL 和 Node.js 沒有直接關係,兩者唯一的共同點就是都使用 JavaScript。
在 ES6 將TypedArray
(二進位制型別陣列)正式加入 ECMA 標準之前,JavaScript 語言本身並沒有標準的處理二進位制資料的能力,Buffer 就是為了彌補這一缺陷。
TypedArray
成為 ECMA 標準之前就已經在 WebGL 領域廣泛使用了。
Node.js 加入 Buffer 的作用主要是為了處理 stream,比如網路流、檔案流等等。Buffer 佔用預申請的一整片記憶體,stream 被消費的速度如果低於接收速度,就會被暫存在緩衝區內,然後被消費者從快取區依序取出消費。
Node.js 中的 Buffer 是 Uint8Array
的子類,Uint8Array
是ECMA 標準中 TypedArray
中的一種資料型別。
console.log(Buffer.__proto__)
// 列印 [Function: Uint8Array]
其實 Node.js 中的 Buffer 與 ECMA 標準的 TypedArray
並沒有直接關係,Node.js 很早期的版本(v0.10.0)版本就支援了 Buffer。Uint8Array
,或者說 ECMA 標準中所有的 TypedArray
都是 JavaScript 引擎提供的一種 API,早期未被加入 ECMA 標準的時候就已經有不少引擎實現了這些 API,而最早使用二進位制型別陣列的場景就是 WebGL。
話說回來,ECMA 標準做的不就是“集百家之長”(修辭手法-反諷)的事嗎哈哈?
然後說到 WebGL 中的 Buffer。
WebGL 有兩種 Buffer 型別:
ARRAY_BUFFER
:頂點屬性資料的 Buffer,用來傳遞任何跟頂點相關的資料,比如座標、顏色等等。這些資料一般是浮點數,最常用的型別是Float32Array
;ELEMENT_ARRAY_BUFFER
:元素索引資料的 Buffer,用來傳遞讀取ARRAY_BUFFER
元素的順序。每個元素必須是整數,使用Uint8Array
,這一點跟 Node.js 中的 Buffer 一致。此 buffer 是可選項,如果不使用的話 ,ARRAY_BUFFER
的元素會被按照 index 依序讀取。
雖然 WebGL 中沒有 stream 的概念(嚴格來說是從開發者的認知層面沒有 stream,底層 OpenGL 處理 buffer 資料的流程中是有 stream 的),但 Buffer 的作用跟 Node.js 是一致的,都是將資料暫存在一整片預申請的記憶體中,供後續程式邏輯消費,區別是消費者不同。
在WebGL渲染管線中,但從CPU到GPU完整的資料傳輸鏈路中,有以下幾種buffer:
- VBO,Vertex Buffer Object,頂點緩衝物件儲存頂點屬性資料,消費者是 shader,嚴格的說是 vertex shader;
- FBO,Fragment Buffer Object,幀緩衝物件可以簡單理解為一個指標集合體,附著 RBO、顏色、紋理等用於渲染的所有資訊;
- RBO,Rendering Buffer Object,渲染緩衝物件儲存 depth(深度)、stencil(模板)值。
FBO 與 RBO、紋理的關係如下圖:
另外一點需要了解的是 buffer 物件從 CPU 流轉到 GPU 的過程,這個過程涉及到匯流排通訊,雖然這些跟 Node.js 沒有一毛錢關係,但是其中的一些實現跟 Node.js 常見八股文面試題「跨程式通訊」有一些相同的理念。
WebGL中buffer最初被建立和寄存在CPU記憶體中,如何讓GPU訪問CPU記憶體呢?回答這個問題之前先介紹幾個基本概念:
- CPU 的記憶體一般稱為 main memory
- GPU 自己的儲存稱為 local memory
在 WebGL/OpenGL 中,頂點資料被建立被寄存在 main memory 中,GPU 需要得到這部分資料進行渲染,但是 main memory 和 local memory 是絕對隔離的,不能互相訪問。
對於整合顯示卡來說,GPU 和 CPU 共享匯流排,GPU 沒有自己獨立的儲存空間,一般是從 CPU 儲存中分配出一塊空間給 GPU 使用,我們把這部分空間姑且叫做視訊記憶體(嚴格來說整合顯示卡沒有視訊記憶體的概念)。為了實現 GPU 和 CPU 資料的共享,業內引入了一種叫做 GART(Graphic Address Remapping Table)的技術,GART簡單說就是一個對映 main memory 和 local memory 地址的表。整合顯示卡的視訊記憶體一般很小,必然是小於記憶體的(一般預設上限是記憶體總量的1/4),OS 將整個 local memory 空間對映到 main memory,維護一個 GART。此時 buffer 資料的流轉如下圖所示:
但是這套流程在獨立顯示卡中是行不通的,因為獨立顯示卡的視訊記憶體非常大,如果使用 GART 將視訊記憶體空間完全對映到 CPU 記憶體中會佔用非常大的記憶體空間,32位系統的整個記憶體空間也就僅僅4GB,如果分出 2GB 給視訊記憶體對映,那就別幹啥了。
這下明白為啥64位系統玩遊戲更爽了吧~
所以對於獨立顯示卡需要另外一套 CPU 與 GPU 的資料共享機制。目前比較普遍的方式是在記憶體中單獨劃出一塊物理空間用於 CPU 和 GPU 之間的資料交換中轉,這部分記憶體空間叫做 pinned memory(鎖定記憶體)。buffer 資料首先會被從 main memory 中拷貝到 pinned memory 中,然後通過 DMA(Direct Memory Access,直接記憶體訪問)機制將資料傳輸到 GPU,整個過程如下:
請注意, pinned memory 是一塊實體記憶體而不是虛擬記憶體,這樣能夠保證DMA的傳輸效能。
這下明白為啥打遊戲一定要加大記憶體了吧~
獨立顯示卡的這套資料交換機制跟 Node.js 八股文「跨程式通訊」的共享記憶體理念很接近,不過複雜度更高一些。
上面這些內容大都是 OpenGL 和計算機底層的機制,對 WebGL 開發者來說是無感知的,具體到涉及 Buffer 的程式碼層面, WebGL 需要比 Node.js 更謹慎的處理 Buffer 的記憶體管理。
Node.js 中 Buffer 在分配記憶體時採用了 slab 預先申請、事後分配機制,這是在底層C++的邏輯,開發者不可控。這套機制能夠提高 Node.js 需要頻繁申請 buffer 記憶體場景下的效能表現。而 WebGL 中並沒有這套機制,需要開發者自行處理。一般的做法是預申請一個容量很大的 buffer,然後使用 gl.bufferSubData
(類似Node.js 的 Buffer.fill
)區域性更新資料,這樣能避免頻繁申請記憶體空間造成的效能損耗。
以上。