我們之前都是通過glNamedBufferData
初始化buffer object,初始化的意思是為buffer object開闢視訊記憶體空間,並填充資料:
GLfloat position[] =
{
-1.0f, -1.0f,
0.0f, 1.0f,
1.0f, -1.0f,
};
GLuint vbo = 0;
glCreateBuffers(1, &vbo);
glNamedBufferData(vbo, sizeof(position), position, GL_STATIC_DRAW);
...
drawTriangle();
glNamedBufferData
一個比較方便的地方就是,如果我們在drawTriangle()
完成之後還想再繪製一個方形,可以複用這個vbo。
... //after drawing triangle
GLfloat position[] =
{
-0.5f, 0.5f,
-0.5f, -0.5f,
0.5f, 0.5f,
0.5f, -0.5f,
};
glNamedBufferData(vbo, 8 * sizeof(GLfloat), position, GL_STATIC_DRAW); //只能等drawTriangle完成之後才能執行
...
drawRect();
一個buffer object可以複用很多次,但是不推薦這麼做。因為這樣會降低GPU的並行力度,起碼我們可以推斷:glNamedBufferData(vbo, 8 * sizeof(GLfloat), position, GL_STATIC_DRAW)
要等待drawTriangle()
完成之後才能執行。有經驗的OpenGL程式設計師都會建立兩個VBO完成這兩次render操作。
但是我想表達的是:對於同一個buffer object,可以多次呼叫glNamedBufferData
為它重新分配空間和指定usage。
那有沒有一種API,一旦初始化完buffer object之後,其size和usage就不得發生改變呢?有,它就是我們今天要講的glNamedBufferStorage
。
glNamedBufferStorage
先看一下這個命令的原型:
void glNamedBufferStorage(GLuint buffer,
GLsizeiptr size,
const void *data,
GLbitfield flags);
前三個引數與glNamedBufferData
相同,最後一個引數是以位組合來表達此buffer的usage,它的值可能是以下標識位組合:
- GL_DYNAMIC_STORAGE_BIT
- GL_MAP_READ_BIT
- GL_MAP_WRITE_BIT
- GL_MAP_PERSISTENT_BIT
- GL_MAP_COHERENT_BIT
我不會去機械的羅列出這些標識位的意思,因為那樣會很無趣。我的理念是用到哪個東西,再講哪個東西。
首先,再次明確一下,官方的說法是:一旦用glNamedBufferStorage
為buffer object分配好空間並指定usage flag之後,該buffer object的size和usage flag就不能再發生改變,不過資料的內容倒是可以改變。
在我看來,這句話說的實在是太輕了,因為哪怕是這樣程式碼:
//以相同的引數呼叫glNamedBufferStorage兩次
glNamedBufferStorage(vbo, sizeof(trianglePosition), trianglePosition, 0);
glNamedBufferStorage(vbo, sizeof(trianglePosition), trianglePosition, 0);
OpenGL都會抱怨產生了一個錯誤:Cannot modify immutable buffer。
哇,您還不如和我說對於同一個buffer object,只能呼叫一次glNamedBufferStorage
呢。不過這樣的嚴格限制,帶來的好處是:比起glNamedBufferData
,glNamedBufferStorage
能夠帶來更好的效能。而且glNamedBufferStorage
的usage flag引數更加現代化,準確的判斷usage flag引數,對於效能的優化以及程式的正確執行都至關重要。比如你呼叫了glNamedBufferStorage
為某個buffer object開闢了空間並初始化了其中的資料,之後你再也不想對這個buffer object進行讀取或者寫入,那麼就可以把usage flag設定為0,這將帶來最好的效能優化。
但是如果我們還想修改這個buffer object中的資料,或者是想把buffer object中的資料讀取至記憶體,該怎麼辦呢?
讀取buffer中的資料
在把buffer object的usage設定為0的情況下,我們可以呼叫glNamedBufferSubData
來讀取buffer中的資料:
glNamedBufferStorage(vbo, sizeof(trianglePosition), trianglePosition, 0); //最後一個引數設定為0
...
glGetNamedBufferSubData(vbo, 0, 9*sizeof(GLfloat), positionPointer);
這樣,vbo中的資料就會從視訊記憶體對映到positionPointer
指向的記憶體空間。
不過這裡還是有一個小細節:呼叫glNamedBufferStorage
之後,OpenGL告訴我這個buffer object的usage hint是GL_STATIC_DRAW,然後我們在某個時機再呼叫glGetNamedBufferSubData
之後,OpenGL就會告訴我這個buffer object的usage hint是GL_DYNAMIC_DRAW。我們都知道GL_STATIC_DRAW是要比GL_DYNAMIC_DRAW的效能更好。
向buffer中寫入資料
我們可以呼叫glNamedBufferSubData
來向buffer object中寫入資料:
glNamedBufferSubData(vbo, 0, 9*sizeof(GLfloat), newTrianglePoints);
不過,初始化buffer object的時候,要把usage設定為GL_DYNAMIC_STORAGE_BIT
glNamedBufferStorage(vbo, sizeof(trianglePosition), trianglePosition, GL_DYNAMIC_STORAGE_BIT);
而且,與"讀取buffer中的資料"中得到的結論一致:呼叫glNamedBufferSubData
之後,OpenGL也會告訴我buffer object的usage hint是GL_DYNAMIC_DRAW。
所以各位同學,哥哥奉勸你們:使用glNamedBufferStorage
初始化buffer object空間之後,儘量就不要再對buffer object進行讀寫了吧。
我嘗試過把usage設定為0,然後向buffer寫入資料:
glNamedBufferStorage(vbo, sizeof(trianglePosition), trianglePosition, 0);
...
glNamedBufferSubData(vbo, 0, 9*sizeof(GLfloat), newTrianglePoints);
OpenGL又告訴我發生了一個錯誤: Buffer contents cannot be modified because the buffer was created without the GL_DYNAMIC_STORAGE_BIT set。雖然儘可能精簡usage flag是一個好習慣,不過前提還是要保證程式的正確性。
另外,雖然glNamedBufferSubData
看起來與glNamedBufferData
有點像,但是它們確實不是同一類API。
glNamedBufferData
和glNamedBufferStorage
算是同一類API,而glNamedBufferSubData
是配合glNamedBufferStorage
使用的。
glMapNamedBufferRange
在早期的OpenGL,我們可以使用glMapBuffer
和glUnmapBuffer
來讀寫buffer object的資料。modern OpenGL有更為高階的API來做這件事:
void *glMapNamedBufferRange(GLuint buffer,
GLintptr offset,
GLsizeiptr length,
GLbitfield access);
此君與glMapNamedBuffer
相比,有兩個優點:
- 能夠將buffer object的一部分(而非全部)資料對映到記憶體空間。
- 能夠以位組合的形式描述訪問策略,
access
引數必須與glNamedBufferStorage
的usage
配合起來使用。
比如我想讀取三角形的頂點位置buffer中的資料,然後進行射線求交:
GLfloat position[] =
{
...
};
glNamedBufferStorage(vbo, 9 * sizeof(GLfloat), position, GL_MAP_READ_BIT); //初始化buffer時,usage flag要包含GL_MAP_READ_BIT才能讀取buffer的內容
...
GLfloat* trianglePosition = (GLfloat*)glMapNamedBufferRange(vbo, 0, 9 * sizeof(GLfloat), GL_MAP_READ_BIT);//最後一個參數列示僅對buffer進行讀操作
//射線求交的虛擬碼
Ray ray;
ray.intersectWithTriangle(trianglePosition);
GLboolean unMap = glUnmapNamedBuffer(vbo); //操作完成,告訴OpenGL我不會再使用trianglePosition指標指向的內容了
assert(unMap);
需要反覆強調的:glNamedBufferStorage
的usage flag引數要與glMapNamedBufferRange
的access flag引數配合起來。
另外,當不再需要glMapNamedBufferRange
返回的資料之後,應儘快進行glUnmapNamedBuffer
。然後檢查其返回值,在某些極端的情況下,可能會unmap失敗。
當我進行讀操作的時候,OpenGL沒有通知我關於buffer的usage hint的訊息。當我進行寫操作的時候,OpenGL通知我此buffer的usage hint為GL_DYNAMIC_DRAW。
到底該使用哪個
既然glNamedBufferSubData(glGetNamedBufferSubData)
和glMapNamedBufferRange
都能讀寫buffer的資料,那問題來了,我們應該用哪個呢?
首先,glMapNamedBufferRange
更具效能優勢。我不去講什麼專業名詞(譬如什麼asynchronous DMA transfer之類的東東),僅從程式碼的角度來分析為什麼glMapNamedBufferRange
速度更快。
首先,我們為glNamedBufferSubData
傳入的指標所指向的記憶體空間是我們自己分配出來的。譬如:
GLfloat* trangleColor = new GLfloat[9];
for (int i = 0; i < 3; ++i)
{
for (int j = 0; j < 3; ++j)
trangleColor[i * 3 + j] = 1.0f;
}
glNamedBufferSubData(vbo, 0, sizeof(GLfloat) * 9, trangleColor);
delete[] trangleColor; //傳輸到buffer之後直接銷燬
如上程式碼,OpenGL無法預料到開發者在呼叫完glNamedBufferSubData
之後,怎麼處理trangleColor
指向的記憶體空間,比如我呼叫完glNamedBufferSubData
之後直接銷燬掉了這部分資料。所以OpenGL只能讓CPU在glNamedBufferSubData
先停下來,集中精力把triangleColor
指向的記憶體空間的內容拷貝到視訊記憶體空間,等glNamedBufferSubData
返回之後(拷貝完成),才能繼續往下執行。
而glMapNamedBufferRange
返回記憶體空間是OpenGL自己管理的,當我們呼叫glUnmapNamedBuffer
之後,可以立即返回,然後在比較閒的時候偷偷的執行拷貝操作,並且這個操作也可以與其它的命令並行執行。
所以如果只是很偶爾地讀寫的很小的資料量,兩者區別可能沒有那麼明顯。但是如果頻繁的讀寫,或者一次讀寫的資料量很大,那麼glMapNamedBufferRange glUnmapNamedBuffer
的效能優勢就非常明顯了。
glMapNamedBufferRange
還有一個優點,比如三角形的頂點資料存放到了磁碟的某個檔案中,現在要用這個檔案的頂點來建立vbo:
//使用glNamedBufferSubData
FILE* f = fopen("position.dat", "rb");
fseek(f, 0, SEEK_END);
long fileSize = std::ftell(f);
fseek(f, 0, SEEK_SET);
glNamedBufferStorage(vbo[0], fileSize, nullptr, GL_DYNAMIC_STORAGE_BIT);
GLchar* position = new GLchar[fileSize]; //申請記憶體空間用來存放檔案的頂點資料
fread(position, 1, fileSize, f);
glNamedBufferSubData(vbo[0], 0, fileSize, position);
delete[] position;
fclose(f);
//使用glMapNamedBufferRange
FILE* f = fopen("position.dat", "rb");
fseek(f, 0, SEEK_END);
long fileSize = std::ftell(f);
fseek(f, 0, SEEK_SET);
glNamedBufferStorage(vbo[0], fileSize, nullptr, GL_MAP_WRITE_BIT);
void* data = glMapNamedBufferRange(vbo[0], 0, fileSize, GL_MAP_WRITE_BIT);
fread(data, 1, fileSize, f);
fclose(f);
glUnmapNamedBuffer(vbo[0]);
利用glMapNamedBufferRange
就可以避免先把檔案的內容拷貝到記憶體中這一步驟。
小結
- 推薦使用
glNamedBufferStorage
初始化buffer object。 - 推薦使用
glMapNamedBufferRange glUnmapNamedBuffer
讀寫buffer中的資料。