轉載請注明:http://www.cnblogs.com/vertexshader/articles/Approach_zero_driver_overhead_1.html
一、引言
2014年GDC(Game Developer Conference)大會上發布了許多嶄新的東西,其中有一些是和圖形接口優化相關的內容,例如Microsoft提出的DirectX 12接口,在降低圖形接口調用上的開銷做了進一步的努力;相應的OpenGL也不甘示弱,Nvidia、AMD和Intel的成員所組成的小組也開始介紹如何減少調用圖形接口方面的開銷,他們表示通過接口的優化能使得OpenGL的效率提升原來的7-15倍。這篇文章的目的就是分析最新出爐的《Approach Zero Driver Overhead》的內容,我希望通過自己對於OpenGL的淺薄了解,讓大家一起來看看如何去優化OpenGL的使用。
二、描述
OpenGL 4.x之后除了渲染管線上增加了曲面細分着色器(Tessellation Shader)和計算着色器(Compute Shader),渲染管線上並無特別明顯的變化,而近些年所定義的有一些規范則是朝着接口優化的方向去努力,例如GL_ARB_multi_bind、GL_ARB_texture_storage等。圖形接口調用所產生的開銷,已經成為了渲染中的瓶頸所在,解決這個開銷問題成為了現今優化的頭等問題,比如AMD的Mantle和Microsoft的DirectX 12,都在着重解決圖形接口調用帶來的開銷問題。談到圖形接口的進化,我聯想到OpenGL的發展:從OpenGL 1.x規范剛發布的時候使用的glVertex*來設定頂點數據,到后來使用Buffer Object來設定頂點數據——圖形接口的規范有一大部分是通過降低調用開銷而進化的。
(一)“驅動的限制”
文章中直接提到了“驅動的限制”,表示應用程序和GPU本可以渲染更多東西的,但是因為“驅動的限制”導致了渲染的瓶頸,而這個限制則是調用圖形接口時所產生的巨大開銷。文章中例舉了導致驅動開銷的幾個原因:①履行API的契約所產生的CPU消耗;②驅動中驗證所產生的開銷;③風險的回避(這里的風險回避指的是什么?我也沒搞清楚)。
文章中也列舉了OpenGL增加開銷的幾種情況,主要的分類則是:①同步所產生的開銷;②開辟空間所產生的開銷;③驗證所產生的開銷;④編輯所產生的開銷。例如使用Buffer Object,所產生的問題則可能會集中在創建Buffer Object和更新Buffer Object時,尤其是對Buffer Object進行更新數據的時候,現今OpenGL有三種接口供更新Buffer Object:
void glBufferSubData(GLenum target, GLintptr offset, GLsizeiptr size, const GLvoid * data); void *glMapBuffer(GLenum target, GLenum access); void *glMapBufferRange(GLenum target, GLintptr offset, GLsizeiptr length, GLbitfield access);
其中glBufferSubData在實踐后都不推薦使用(其實這個接口在更新小量數據的時候有優勢),因為其需要在Client端創建一個內存塊,然后將數據從Client端復制到Server端,存在一個復制數據所產生的開銷。而glMapBuffer和glMapBufferRange成為相應較為效率的更新方式,通過在Client端創建一個指針映射到Sever端的數據並且返回這個指針,應用程序可以通過這個指針來修改存儲的數據,當調用glUnmap的時候,數據就更新完畢了。但是這樣也存在一個問題,尤其是Uniform Buffer Object中,應用程序可能只需要更新其中的一部分,並且存在多個Uniform Buffer Object需要在渲染的時候更新;或許模型數據需要動態的修改,那么則需要每幀都進行glMapBuffer或glMapBufferRange操作對數據進行更新。兩種可能的情況下,都需要對Buffer Object進行多次的Map操作,所帶來的性能問題不言而喻。
還有一種情況則是OpenGL中對Object的綁定,例如Framebuffer Object、Program Object、Texture Object和Buffer Object,都會存在一種驗證機制,例如Framebuffer Completeness,Shader Compilation,Mipmap Completeness,這些Object在綁定的時候都會因為驗證機制導致消耗CPU時間,在某些情況下可能導致一定的性能問題。還有就是資源的綁定所產生的開銷,例如將多個Texture Object綁定到Texture Unit上,以及將多個Buffer Object綁定到Uniform Buffer Object的Binding Slot上去。
為了解決這些問題,小組提供了幾個可以參考的解決辦法,例如使用Buffer Storage和Texture Array,以及Multi-Draw Indirect。
(二)Buffer Storage
OpenGL 4.4所提供的一個擴展就是GL_ARB_buffer_storage擴展,提供了對Buffer Object創建Immutable Storage的方法,以及對Buffer Object更加底層的控制。Buffer Object的主要用途是作為Vertex/Index Buffer Object、Pixel Buffer Object或者是Uniform Buffer Object,在文中提到如果在渲染的時候需要動態的模型所遇到的問題,也就是作為Vertex/Index Buffer Object存儲數據時需要不斷進行更新的問題。前面提到glMapBuffer或者glMapBufferRange是比較消耗CPU時間的(在我的機器上居然達到了30207.993μs),如果每一幀對動態的模型數據進行glMapBuffer操作之后再更新數據,一個場景中如果有多個動態模型數據,這個過程將造成大量的glMapBuffer操作,所帶來的效率降低是明顯的。文章中所提到的解決辦法則是使用Persistent-mapped Buffer,按照字面意思就是持續不斷映射的緩沖區!讓我們先來細看一下Buffer Storage也就是Immutable Storage的具體細節。
GL_ARB_buffer_object提供了全新的接口來創建Immutable Storage:
void glBufferStorage(GLenum target, GLsizeiptr size, const GLvoid * data, GLbitfield flags);
接口的<target>,<size>和<data>與glBufferData的參數無異,而最大的區別就是最后<flags>,代表使用數據存儲的目的,下面的列表列出了相關的參數:
標識符 |
內容 |
GL_DYNAMIC_STORAGE_BIT |
允許glBufferSubData更新數據內容 |
GL_MAP_READ_BIT |
允許客戶端通過映射讀取數據內容 |
GL_MAP_WRITE_BIT |
允許客戶端通過映射寫入數據內容 |
GL_MAP_PERSISTENT_BIT |
允許服務端在緩沖被映射的狀態下讀寫數據 |
GL_MAP_COHERENT_BIT |
客戶端(服務端)更新的數據會在服務端(客戶端)立即可見的 |
GL_CLIENT_STORAGE_BIT |
指定數據在客戶端存儲 |
相應的,通過這個接口創建Buffer Object之后,其Buffer Object State略有所不同:
狀態名 |
glBufferData的值 |
glBufferStorage的值 |
GL_BUFFER_SIZE |
<size> |
<size> |
GL_BUFFER_USAGE |
<usage> |
GL_DYNAMIC_DRAW |
GL_BUFFER_ACCESS |
GL_READ_WRITE |
GL_READ_WRITE |
GL_BUFFER_ACCESS_FLAGS |
0 |
0 |
GL_BUFFER_IMMUTABLE_STORAGE |
GL_FALSE |
GL_TRUE |
GL_BUFFER_MAPPED |
GL_FALSE |
GL_FALSE |
GL_BUFFER_MAP_POINTER |
NULL |
NULL |
GL_BUFFER_MAP_OFFSET |
0 |
0 |
GL_BUFFER_MAP_LENGTH |
0 |
0 |
GL_BUFFER_STORAGE_FLAGS |
GL_MAP_READ_BIT | GL_MAP_WRITE_BIT | GL_DYNAMIC_STORAGE |
<flags> |
其中兩個重要的參數便是GL_MAP_PERSISTENT_BIT和GL_MAP_COHERENT_BIT,GL_MAP_PERSISTENT_BIT的意義是當Buffer Object中的GL_BUFFER_MAPEED狀態是GL_TRUE時也就是Buffer Object被映射的時候,也可以被服務端讀寫數據,也就是延長了Map Pointer的時效。更加詳細地說就是,以前當Buffer Object處於Mapped狀態的時候,其只能進行客戶端的讀寫操作,而不能進行其他的操作例如glDrawElements這樣的繪制命令;現在通過Immutable Storage設置GL_MAP_PERSISTENT_BIT位,數據可以在Mapped狀態時也可以被服務端讀寫了,可以調用glDrawElements或者glCopyBufferSubData,而不會產生任何的錯誤。GL_MAP_COHERENT_BIT代表的是當Buffer Object被Map的時候,不管是客戶端和服務端的哪一端更新了數據,更新的數據都會立即在另一端可見,其和GL_MAP_PERSISTENT_BIT一般是聯合起來一起使用的。這就好像是把Buffer Object變成了一般的“數組”一樣。
文章中提到的Persistent-mapped Buffer就是<flags>設置為GL_MAP_PERSISTENT_BIT和GL_MAP_COHERENT_BIT的Immutable Buffer Storage,其減少驅動開銷的辦法是在創建Buffer Object之后就使用glMapBufferRange獲得Map Pointer並存儲起來,因為Map Pointer的持久有效性,程序只需要glMapBufferRange一次以后只要再通過這個指針更新數據就好,而不需要因為其他接口不允許Mapped狀態而使用glUnmapBuffer棄用指針,然后下次更新再次獲得指針的辦法,這樣每幀調用glMapBufferRange的開銷直接就被消除了(其實我覺得奇怪的地方就是,如果沒有使用Immutable Storage,在Map之后獲得指針后調用glUnmapBuffer,其指針還是有效的並且還可以更新數據,在Nvidia、AMD和Intel的顯卡上都是如此,貌似glUnmapBuffer只是簡單的更改了GL_BUFFER_MAPPED狀態,雖然在規范中提到千萬不要在glUnmapBuffer之后用這個指針)。代碼范例如下:
// 創建Persistent-mapped buffer對象 glGenBuffers(1, &buf); glBindBuffer(GL_ARRAY_BUFFER, buf); GLbitfield flags= access_bit | GL_MAP_PERSISTENT_BIT // 處在Mapped狀態也能使用 | GL_MAP_COHERENT_BIT; // 數據對GPU立即可見 glBufferStorage(GL_ARRAY_BUFFER, size, data, flags; // 只需要Map一次就好,以后保存這個指針用於更新,不需要再Map GLvoid *ptr = glMapBufferRange(GL_ARRAY_BUFFER, 0, size, flags); // 繪制過程 // 通過<ptr>直接更新數據 UpdateBufferObject(ptr); // 設置渲染需要的數據,然后直接調用Draw Call,不需要glUnmapBuffer glDrawElements(...);
這個GL_MAP_PERSISTENT不僅僅影響的是Draw Call,其影響的接口非常之多,影響的內容都是Buffer Object在Mapped狀態也可以被使用,可以分為如下幾種情況:
影響的情況 |
影響的接口 |
Pixel Buffer Object相關 |
glReadPixels glCompressedTexSubImage* glGetCompressedTexImage glTexSubImage* glGetTexImage |
Buffer Object相關 |
glBufferSubData glClearBufferSubData glClearBufferData glInvalidBufferSubData glInvalidBufferData glCopyBufferSubData |
Draw Call相關 |
glDrawArrays glDrawArraysIndirect glDrawArraysInstanced glDrawArrayInstancedBaseInstanced glDrawElements glDrawElementsBaseVertex glDrawElementsIndirect glDrawElementsInstanced glDrawElementsInstancedBaseInstanced glDrawElementsInstancedBaseVertex glDrawElementsInstancedBaseVertexInstance glDrawRangeElements glDrawRangeElementsBaseVertex glDrawTransformFeedback, glDrawTransformFeedbackInstanced glDrawTransformFeedbackStream glDrawTransformFeedbackStreamInstanced |
通過這個擴展,可以想到不管是創建什么Buffer Object,是Index/Vertex Buffer當做頂點數據來源,還是Pixel Buffer Object作為紋理的像素數據來源,還是Uniform Buffer Object然后作為Shader Program的數據來源,可以想到對於動態的Index/Vertex Buffer Object或者是動態的Uniform Buffer Object,應當首先設置GL_MAP_PERSISTENT_BIT和GL_MAP_COHERENT_BIT位,然后首先進行glMapBufferRange操作獲得指針然后保存,日后更新只需要使用這個指針即可。在每幀中,動態模型的數據更新,或者是有很多紋理更新還有就是着色器參數的更新,都能拋棄過多的glMapBufferRange操作,從而減少每幀的耗時(glCopyBufferSubData所產生的Server端的數據更新可能沒法立即更新到Map Pointer,可以使用glFinish()來強制完成所有的命令)!對於靜態的Index/Vertex Buffer和Uniform Buffer Object,盡可能使用原來的方法;而對於Persistent-mapped buffer,要盡可能的減少Copy操作(會帶來同步的問題)。