天天看點

《OpenGL程式設計指南(原書第9版)》——3.2 OpenGL緩存資料

幾乎所有使用OpenGL完成的事情都用到了緩存buffers中的資料中。OpenGL的緩存表示為緩存對象(buffer object)。第1章已經簡要地介紹了緩存對象的意義。不過,這一節将稍微深入到緩存對象的方方面面當中,包括它的種類、建立方式、管理和銷毀,以及與緩存對象有關的一些最優解決方案。

3.2.1 建立與配置設定緩存

與OpenGL中的很多其他實作類似,緩存對象也是使用GLuint的值來進行命名的。這個值可以使用glCreateBuffers()指令來建立。我們已經在第1章介紹過這個函數了,但是在這裡會再次給出它的原型,以友善讀者參考。

void glCreateBuffers(GLsizei n, GLuint* buffers);

傳回n個目前未使用的緩存對象名稱(每個都表示一個新建立的緩存對象),并儲存到buffers數組中。

調用glCreateBuffers()完成之後,我們将在buffers中得到一個緩存對象名稱的數組。這些緩存對象已經被建立了,但是還沒有連接配接到任何存儲空間。使用者需要使用glNamedBufferStorage()為每個緩存對象配置設定存儲空間。擁有存儲空間之後,我們就可以綁定對象到緩存目标了。可用的緩存目标(target)如表3-2中所示。

《OpenGL程式設計指南(原書第9版)》——3.2 OpenGL緩存資料

緩存對象的建立,實際上就是通過調用glCreateBuffers()函數生成一系列名稱,然後通過glBindBuffer()将一個名稱綁定到表3-2中的一個目标來完成的。第1章當中已經介紹過glCreateBuffers()和glBindBuffer()函數,不過這裡将再次給出函數的原型,以保證文字的完整性。

void glBindBuffer(GLenum target, GLuint buffer);

将名稱為buffer的緩存對象綁定到target所指定的緩存結合點。target必須是OpenGL支援的緩存綁定目标之一,buffer必須是通過glCreateBuffers()配置設定的名稱。如果buffer是第一次被綁定,那麼它所對應的緩存對象也将同時被建立。

好了,現在我們已經将緩存對象綁定到表3-2中的某一個目标上了,然後呢?新建立的緩存對象的預設狀态,相當于是不存在任何資料的一處緩存區域。如果想要将它實際使用起來,就必須向其中輸入一些資料才行。

3.2.2 向緩存輸入和輸出資料

将資料輸入和輸出OpenGL緩存的方法有很多種。比如直接顯式地傳遞資料,又比如用新的資料替換緩存對象中已有的部分資料,或者由OpenGL負責生成資料然後将它記錄到緩存對象中。向緩存對象中傳遞資料最簡單的方法就是在配置設定記憶體的時候讀入資料。這一步可以通過glNamedBufferStorage()函數來完成。下面再次給出glNamedBufferStorage()的原型。

void glNamedBufferStorage(GLuint buffer, GLsizeiptr size, const void *data, GLbitf?ield f?lags);

為緩存對象buffer配置設定size大小(機關為位元組)的存儲空間。如果參數data不是NULL,那麼将使用data所在的記憶體區域的内容來初始化整個空間。f?lags用來設定緩存的預期用途資訊。這些f?lags辨別量在使用者程式和OpenGL之間建構了協定,允許OpenGL盡可能極緻地優化緩存的存儲空間。

對于glNamedBufferStorage()來說,最重要的參數可能就是f?lags參數了。f?lags是一系列辨別量的按位合并的結果,這些辨別量如表3-3所示。

《OpenGL程式設計指南(原書第9版)》——3.2 OpenGL緩存資料
《OpenGL程式設計指南(原書第9版)》——3.2 OpenGL緩存資料

準确判斷f?lags參數,對于性能的優化以及正确執行都非常重要。這個參數向OpenGL傳達了有關使用者如何使用緩存的關鍵資料。

緩存的部分初始化

假設有一個包含部分頂點資料的數組,另一個數組則包含一部分顔色資訊,還有一個數組包含紋理坐标或者别的什麼資料。你需要将這些資料進行緊湊的打包,并且存入一個足夠大的緩存對象讓OpenGL使用。在記憶體中數組之間可能是連續的,也可能不連續,是以無法簡單地使用glNamedBufferStorage()來存儲資料,以及一次性地更新所有的資料。此外,如果使用glNamedBufferStorage()進行更新的話,那麼首先是頂點資料,然後緩存的大小與頂點資料的大小一緻,并且也就不再有空間去存儲顔色或者紋理坐标資訊了。是以我們需要引入新的glNamedBufferSubData()函數。

void glNamedBufferSubData(GLuint buffer, GLintptr offset, GLsizeiptr size, const void *data);

使用新的資料替換緩存對象buffer中的部分資料。緩存中從offset位元組處開始需要使用位址為data、大小為size的資料塊來進行更新。如果offset和size的總和超出了緩存對象綁定資料的範圍,那麼将産生一個錯誤。

緩存buffer中存儲的資料必須經過glNamedBufferStorage()初始化,并且辨別量應當設定為GL_DYNAMIC_STORAGE_BIT。

如果将glNamedBufferStorage()和glNamedBufferSubData()結合起來使用,那麼我們就可以對一個緩存對象進行配置設定和初始化,然後将資料更新到它的不同區塊當中。一個相應的示例可以參見例3.1。

例3.1 使用glNamedBufferStorage()來初始化緩存對象

《OpenGL程式設計指南(原書第9版)》——3.2 OpenGL緩存資料
《OpenGL程式設計指南(原書第9版)》——3.2 OpenGL緩存資料
《OpenGL程式設計指南(原書第9版)》——3.2 OpenGL緩存資料

如果隻是希望将緩存對象的資料清除為一個已知的值,那麼也可以使用glClear-NamedBufferData()或者glClearNamedBufferSubData()函數。它們的原型如下所示:

void glClearNamedBufferData(GLuint buffer, GLenum internalformat, GLenum format, GLenum type, const void* data);

void glClearNamedBufferSubData(GLuint buffer, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format, GLenum type, const void* data);

清除緩存對象中所有或者部分資料。名為buffer的緩存存儲空間将使用data中存儲的資料進行填充。format和type分别指定了data對應資料的格式和類型。

首先将資料被轉換到internalformat所指定的格式,然後填充緩存資料的指定區域範圍。

對于glClearNamedBufferData()來說,整個區域都會被指定的資料所填充。而對于glClearNamedBufferSubData()來說,填充區域是通過offset和size來指定的,它們分别給出了以位元組為機關的起始偏移位址和大小。

glClearNamedBufferData()和glClearNamedBufferSubData()函數允許我們初始化緩存對象中存儲的資料,并且不需要保留或者清除任何一處系統記憶體。

緩存對象中的資料也可以使用glCopyNamedBufferSubData()函數互相進行拷貝。與glNamedBufferSubData()函數對較大緩存中的資料依次進行組裝的做法不同,此時我們可以使用glNamedBufferStorage()将資料更新到獨立的緩存當中,然後将這些緩存直接用glCopyNamedBufferSubData()拷貝到一個較大的緩存中。你也可以配置設定一系列緩存對象,然後循環對它們進行兩兩操作,確定正在寫入的資料不會同時被使用,進而實作拷貝資料的疊加。

glCopyNamedBufferSubData()的原型如下所示:

void glCopyNamedBufferSubData(GLuint readBuffer, GLuint writeBuffer, GLintptr readoffset, GLintptr writeoffset, GLsizeiptr size);

将名為readBuffer的緩存對象的一部分存儲資料拷貝到名為writeBuffer的緩存對象的資料區域上。readBuffer對應的資料從readoffset位置開始複制size個位元組,然後拷貝到writeBuffer對應資料的writeoffset位置。如果readoffset或者writeoffset與size的和超出了綁定的緩存對象的範圍,那麼OpenGL會産生一個GL_INVALID_VALUE錯誤。

glCopyNamedBufferSubData()可以在兩個目标對應的緩存之間拷貝資料,而GL_COPY_READ_BUFFER和GL_COPY_WRITE_BUFFER這兩個目标正是為了這個目的而生。它們不能用于其他OpenGL的操作當中,并且如果将緩存與它們進行綁定,并且隻用于資料的拷貝和存儲目的,不影響OpenGL的狀态也不需要記錄拷貝之前的目标區域資訊的話,那麼整個操作過程都是可以保證安全的。

讀取緩存的内容

我們可以通過多種方式從緩存對象中回讀資料。第一種方式就是使用glGet-NamedBufferSubData()函數。這個函數可以從綁定到某個目标的緩存中回讀資料,然後将它放置到應用程式保有的一處記憶體當中。glGetNamedBufferSubData()的原型如下所示:

void glGetNamedBufferSubData(GLenum target, GLintptr offset, GLsizeiptr size, void* data);

傳回目前名為buffer的緩存對象中的部分或者全部資料。起始資料的偏移位元組位置為offset,回讀的資料大小為size個位元組,它們将從緩存的資料區域拷貝到data所指向的記憶體區域中。如果緩存對象目前已經被映射,或者offset和size的和超出了緩存對象資料區域的範圍,那麼将提示一個錯誤。

如果我們使用OpenGL生成了一些資料,然後希望重新擷取到它們的内容,那麼此時應該使用glGetNamedBufferSubData()。這樣的例子包括在GPU級别使用transform feedback處理頂點資料,以及将幀緩存或者紋理資料讀取到像素緩存對象(Pixel Buffer Object)中。後文将依次給出這些内容的具體介紹。當然,我們也可以使用glGetBufferSubData()簡單地将之前存入到緩存對象中的資料讀回到記憶體中。

3.2.3 通路緩存的内容

目前為止,本節給出的所有函數(glNamedBufferData()、glCopyNamedBufferSubData()和glGetNamedBufferSubData())都存在同一個問題,就是它們都會導緻OpenGL進行一次資料的拷貝操作。glNamedBufferSubData()會将應用程式記憶體中的資料拷貝到OpenGL管理的記憶體當中。顯而易見glCopyNamedBufferSubData()會将源緩存中的内容進行一次拷貝(到另一個緩存或同一個緩存的不同位置)。glGetNamedBufferSubData()則是将緩存對象的資料拷貝到應用程式記憶體中。根據硬體的配置,其實也可以通過擷取一個指針的形式,直接在應用程式中對OpenGL管理的記憶體進行通路。當然,擷取這個指針的對應函數就是glMapBuffer()。

void* glMapBuffer(GLenum target, GLenum access);

将目前綁定到target的緩存對象的整個資料區域映射到用戶端的位址空間中。之後可以根據給定的access政策,通過傳回的指針對資料進行直接讀或者寫的操作。如果OpenGL無法将緩存對象的資料映射出來,那麼glMapBuffer()将産生一個錯誤并且傳回NULL。發生這種情況的原因可能是與系統相關的,比如可用的虛拟記憶體過低等。

當我們調用glMapBuffer()時,這個函數會傳回一個指針,它指向綁定到target的緩存對象的資料區域所對應的記憶體。注意這塊記憶體隻是對應于這個緩存對象本身—它不一定就是圖形處理器用到的記憶體區域。access參數指定了應用程式對于映射後的記憶體區域的使用方式。它必須是表3-4中列出的辨別符之一。

《OpenGL程式設計指南(原書第9版)》——3.2 OpenGL緩存資料

如果glMapBuffer()無法映射緩存對象的資料,那麼它将傳回NULL。access參數相當于使用者程式與OpenGL對記憶體通路的一個約定。如果使用者違反了這個約定,那麼将産生很不好的結果,例如寫緩存的操作将被忽略,資料将被破壞,甚至使用者程式會直接崩潰。

當你要求映射到應用程式層面的資料正處于無法通路的記憶體當中,OpenGL可能會被迫将資料進行移動,以保證能夠擷取到資料的指針,也就是你期望的結果。與之類似,當你完成了對資料的操作,以及對它進行了修改,那麼OpenGL将再次把資料移回到圖形處理器所需的位置上。這樣的操作對于性能上的損耗是比較高的,是以必須特别加以對待。

如果緩存已經通過GL_READ_ONLY或者GL_READ_WRITE通路模式進行了映射,那麼緩存對象中的資料對于應用程式就是可見的。我們可以回讀它的内容,将它寫入磁盤檔案,甚至直接對它進行修改(如果使用了GL_READ_WRITE作為通路模式的話)。如果通路模式為GL_READ_WRITE或者GL_WRITE_ONLY,那麼可以通過OpenGL傳回的指針向映射記憶體中寫入資料。當結束資料的讀取或者寫入到緩存對象的操作之後,必須使用glUnmapNamedBuffer()執行解除映射操作,它的原型如下所示:

GLboolean glUnmapNamedBuffer(Gluint buffer);

解除glMapNamedBufferRange()針對緩存對象buffer建立的映射。如果對象資料的内容在映射過程中沒有發生損壞,那麼glUnmapBuffer()将傳回GL_TRUE。發生損壞的原因通常與系統相關,例如螢幕模式發生了改變,這會影響圖形記憶體的可用性。這種情況下,函數的傳回值為GL_FALSE,并且對應的資料内容是不可預測的。應用程式必須考慮到這種幾率較低的情形,并且及時對資料進行重新初始化。

如果解除了緩存的映射,那麼之前寫入到OpenGL映射記憶體中的資料将會重新對緩存對象可見。這句話的意義是,我們可以先使用glNamedBufferStorage()配置設定資料空間,并且在data參數中直接傳遞NULL,之後進行映射并且直接将資料寫入,最後解除映射,進而完成了資料向緩存對象傳遞的操作。例3.2所示就是一個将檔案内容讀取并寫入到緩存對象的例子。

例3.2 使用glMapBuffer()初始化緩存對象

《OpenGL程式設計指南(原書第9版)》——3.2 OpenGL緩存資料

在例3.2中,檔案的所有内容都在單一操作中被讀入到緩存對象當中。緩存對象建立時的大小與檔案是相同的。當緩存映射之後,我們就可以直接将檔案内容讀入到緩存對象的資料區域當中。應用程式端并沒有拷貝的操作,并且如果資料對于應用程式和圖形處理器都是可見的,那麼OpenGL端也沒有進行任何拷貝的操作。

使用這種方式來初始化緩存對象可能會帶來顯著的性能優勢。其理由如下:如果調用glNamedBufferStorage()或者glNamedBufferSubData(),當傳回這些函數後,我們可以對傳回的記憶體區域中的資料進行任何操作—釋放它,使用它做别的事情—都是可以的。這也就是說,這些函數在完成後不能與記憶體區域再有任何瓜葛,是以必須采取資料拷貝的方式。但是,如果調用glMapNamedBufferRange(),它所傳回的指針是OpenGL端管理的。當調用glUnmapNamedBuffer()時,OpenGL依然負責管理這處記憶體,而使用者程式與這處記憶體已經不再有瓜葛了。這樣的話即使資料需要移動或者拷貝,OpenGL都可以在調用glUnmapNamedBuffer()之後才開始這些操作并且立即傳回,而内容操作是在系統的空閑時間之内完成,不再受到應用程式的影響。是以,OpenGL的資料拷貝操作與應用程式之後的操作(例如建立更多的緩存,讀取别的檔案,等等)實際上是同步進行的。如果不需要進行拷貝的話,那麼結果就再好不過了!此時在本質上解除映射的操作相當于是對空間的釋放。

異步和顯式的映射

為了避免glMapBuffer()可能造成的緩存映射問題(例如應用程式錯誤地指定了access參數,或者總是使用GL_READ_WRITE),glMapNamedBufferRange()函數使用額外的辨別符來更精确地設定通路模式,glMapNamedBufferRange()函數的原型如下所示:

void * glMapNamedBufferRange(GLuint buffer, GLintptr offset, GLsizeiptr length, GLbitf?ield access);

将緩存對象資料的全部或者一部分映射到應用程式的位址空間中。buffer設定了緩存對象的名字。offset和length一起設定了準備映射的資料範圍(機關為位元組)。access是一個位域辨別符,用于描述映射的模式。

對于glMapNamedBufferRange()來說,access位域中必須包含GL_MAP_READ_BIT和GL_MAP_WRITE_BIT中的一個或者兩個,以确認應用程式是否要對映射資料進行讀操作、寫操作,或者兩者皆有。此外,access中還可以包含一個或多個其他的辨別符,如表3-5所示。

《OpenGL程式設計指南(原書第9版)》——3.2 OpenGL緩存資料

正如你在表3-5中看到的這些辨別符所提示的,對于OpenGL資料的使用以及資料通路時的同步操作,這個指令可以實作一個更精确的控制過程。

如果打算通過GL_MAP_INVALIDATE_RANGE_BIT或者GL_MAP_INVALIDATE_BUFFER_BIT辨別符來實作緩存資料的無效化,那麼也就意味着OpenGL可以對緩存對象中任何已有的資料進行清理。除非你确信自己要同時使用GL_MAP_WRITE_BIT辨別符對緩存進行寫入操作,否則不要設定這兩個辨別符中的任意一個。如果你設定了GL_MAP_INVALIDATE_RANGE_BIT的話,你的目的應該是對某個區域的整體進行更新(或者至少是其中對你的程式有意義的部分)。如果設定了GL_MAP_INVALIDATE_BUFFER_BIT,那麼就意味着你不打算再關心那些沒有被映射的緩存區域的内容了,或者你準備在後繼的映射當中對緩存中剩下的部分進行更新。由于此時OpenGL是可以抛棄緩存資料中剩餘的部分,是以即使你将修改過的資料重新合并到原始緩存中也沒有什麼意義了。是以,如果打算對映射緩存的第一個部分使用GL_MAP_INVALIDATE_BUFFER_BIT,然後對緩存其他的部分使用GL_MAP_INVALIDATE_RANGE_BIT,那麼應該是一個不錯的想法。

GL_MAP_UNSYNCHRONIZED_BIT辨別符用于禁止OpenGL資料傳輸和使用時的自動同步機制。沒有這個标志符的話,OpenGL會在使用緩存對象之前完成任何正在執行的指令。這一步與OpenGL的管線有關,是以可能會造成性能上的損失。如果可以確定之後的操作可以在真正修改緩存内容之前完成(不過在調用glMapNamedBufferRange()之前這并不是必須的),例如調用glFinish()或者使用一個同步對象(參見11.3節),那麼OpenGL也就不需要專門為此維護一個同步功能了。

最後,GL_MAP_FLUSH_EXPLICIT_BIT辨別符表明了應用程式将通知OpenGL它修改了緩存的哪些部分,然後再調用glUnmapNamedBuffer()。通知的操作可以通過glFlush-MappedBufferRange()函數的調用來完成,其原型如下:

void glFlushMappedNamedBufferRange(GLuint buffer, GLintptr offset, GLsizeiptr length);

通知OpenGL,映射緩存buffer中由offset和length所劃分的區域已經發生了修改,需要立即更新到緩存對象的資料區域中。

我們可以對緩存對象中獨立的或者互相重疊的映射範圍多次調用glFlushMapped-NamedBufferRange()。緩存對象的範圍是通過offset和length劃分的,這兩個值必須位于緩存對象的映射範圍之内,并且映射範圍必須通過glMapNamedBufferRange()以及GL_MAP_FLUSH_EXPLICIT_BIT辨別符來映射。當執行這個操作之後,會假設OpenGL對于映射緩存對象中指定區域的修改已經完成,并且開始執行一些相關的操作,例如重新激活資料的可用性,将它拷貝到圖形處理器的顯示記憶體中,或者進行重新整理,資料緩存的重新更新等。就算緩存的一部分或者全部還處于映射狀态下,這些操作也可以順利完成。這一操作對于OpenGL與其他應用程式操作的并行化處理是非常有意義的。舉例來說,如果需要從檔案加載一個非常龐大的資料塊并将他們送入緩存,那麼需要在緩存中配置設定足夠囊括整個檔案大小的區域,然後讀取檔案的各個子塊,并且對每個子塊都調用一次glFlushMappedNamedBufferRange()。然後OpenGL就可以與應用程式并行地執行一些工作,從檔案讀取更多的資料并且存入下一個子塊當中。

通過這些辨別符的不同混合方式,我們可以對應用程式和OpenGL之間的資料傳輸過程進行優化,或者實作一些進階的技巧,例如多線程或者異步的檔案操作。

3.2.4 丢棄緩存資料

進階技巧

如果已經完成了對緩存資料的處理,那麼可以直接通知OpenGL我們不再需要使用這些資料。例如,如果我們正在向transform feedback的緩存中寫入資料,然後使用這些資料進行繪制。如果最後通路資料的是繪制指令,那麼我們就可以及時通知OpenGL,讓它适時地抛棄資料并且将記憶體用作其他用途。這樣OpenGL的實作就可以完成一些優化工作,諸如緊密的記憶體配置設定政策,或者避免系統與多個GPU之間産生代價高昂的拷貝操作。

如果要抛棄緩存對象中的部分或者全部資料,那麼我們可以調用glInvalidateBufferData()或者glInvalidateBufferSubData()函數。這兩個函數的原型如下所示:

void glInvalidateBufferData(GLuint buffer);

void glInvalidateBufferSubData(GLuint buffer, GLintptr offset, GLsizeiptr length);

通知OpenGL,應用程式已經完成對緩存對象中給定範圍内容的操作,是以可以随時根據實際情況抛棄資料。glInvalidateBufferSubData()會抛棄名稱為buffer的緩存對象中,從offset位元組處開始共length位元組的資料。glInvalidateBufferData()會直接抛棄整個緩存的資料内容。

注意,從理論上來說,如果調用glBufferData()并且傳入一個NULL指針的話,那麼所實作的功能與直接調用glInvalidateBufferData()是非常相似的。這兩個方法都會通知OpenGL實作可以安全地抛棄緩存中的資料。但是,從邏輯上glBufferData()會重新配置設定記憶體區域,而glInvalidateBufferData()不會。根據OpenGL的具體實作,通常調用glInvalidateBufferData()的方法會更為優化一些。此外,glInvalidateBufferSubData()也是唯一一個可以抛棄緩存對象中的區域資料的方法。