opengl幀緩存
A. OpenGL ES基礎理論 (一) —— 緩存、幀緩存、上下文與坐標系等
程序會將3D場景數據保存到硬體RAM中,嵌入式系統的中央處理單元有專門為其分配的RAM,在圖形處理的過程中,GPU也有專門為其分配的RAM,使用現代硬體渲染3D圖形的速度幾乎彎完全取決於不用的內存區塊被訪問的方式。
先看一下下圖。
OpenGL ES 是 一 種 軟 件 技 術。OpenGL ES 部分運行在 CPU 上,部分運行在 GPU 上。 OpenGL ES 橫跨在兩個處理器之間,協調兩個內存區域之間的數據交換。上圖中箭頭代表了與3D渲染相關硬體組件之間的數據交換,每個箭頭也代表著一個渲染性能的瓶頸。
從一個內存區域復制數據到另一個內存區域速度是相對較慢的。更糟糕的是,除非非常小心,在內存復制發生的時候 GPU 和 CPU 都不能把內存另作它用。因此內存區域之間的數據交換需要盡量避免。
最新的 OpenGL ES 為了支持新改進的方法拋棄了對於舊式的低效的內存復制操作的支持。
OpenGL ES 為兩個內存區域間的數據交換定義了 緩存(buffers) 的概念。緩存是指圖形處理器能夠控制和管理的連續 RAM。程序從 CPU 的內存復制數據到 OpenGL ES 的緩存。在 GPU 取得一個緩存的所有權以後,運行在 CPU 中的程序理想情況下將不 再接觸這個緩存。通過控制獨占的緩存,GPU 就能夠盡可能以最有效的方式讀寫內存。 圖形處理器把它處理大量數據的能力非同步同時地應用到緩存上,這意味著在 GPU 使用 緩存中的數據工作的同時,運行在 CPU 中的程序可以繼續執行。
幾乎所有的程序提供給GPU的數據都應該放入緩存中,為緩存提供數據,需要如下7個步驟:
理想情況下,每個生成的緩存都可以使用一個相當長的時間。下面看一下上面幾個步驟的OpenGL ES的C語言函數的實現。
GPU 需要知道應該在內存中的哪個位置存儲渲染出來的 2D 圖像像素數據。就像 為 GPU 提供數據的緩存一樣,接收渲染結果的緩沖區叫做幀緩存 (frame buffer) 。程 序會像任何其他種類的緩存一樣生成、綁定、刪除幀緩存。但是幀緩存不需要初始 化,因為渲染指令會在適當的時候替換緩存的內容。幀緩存會在被綁定的時候隱式開 啟,同時 OpenGL ES 會自動地根據特定平台的硬體配置和功能來設置數據的類型和偏移。
可以同時存在很多幀緩存,並且可以通過 OpenGL ES 讓 GPU 把渲染結果存儲到 任意數量的幀緩存中。但是,屏幕顯示像素要受到保存在前幀緩存 (front frame buffer) 的特定幀緩存中的像素顏色元素的控制。程序和操作系統很少會直接渲染到前幀緩存中,因為那樣會讓用戶看到正在渲染中的還沒渲染完成的圖像。相反,程序和操作系統 會把渲染結果保存到包括後幀緩存 (back frame buffer) 在內的其他幀緩存中。當渲染後的後幀緩存包含一個完成的圖像時,前幀緩存與後幀緩存幾乎會瞬間切換。後幀緩存會變成新的前幀緩存,同時舊的前幀緩存會變成後幀緩存。
用於配置 OpenGL ES 的保存在特定平台的軟體數據結構中的信息會被封裝到一個 OpenGL ES 上下文(context)中。上下文中的 信息可能會被保存在 CPU 所控制的內存中,也可能會被保存在 GPU 所控制的內存中。 OpenGL ES 會按需在兩個內存區域之間復制信息,知道何時發生復制有助於程序的優化。
OpenGL ES 總是開始於一個矩 形的笛卡兒坐標系,如下圖所示。
OpenGL ES 坐標是以浮點數來存儲的。現代 GPU 對浮點運算做了專門的優化,即 使是使用其他數據類型的頂點也會被轉換成浮點值。
B. GPUImage(四):你們處理的都是GPUImageFramebuffer類型
在使用GPUImage處理圖片或者視頻時,並不能直接對iOS官方定義的的UIImage、CGImage、CIImage進行操作。那麼如果要使用GPUImage的話,第一步操作就是把你想處理的數據類型用一個GPUImage定義的載體承載,才能在GPUImage的處理鏈中進行一步一步的操作,這個載體就是GPUImageFramebuffer。我們把一張圖像的像素等攜帶的全部信息看成好幾種散裝液體顏料的集合,系統提供了UIImage(最常用的圖像類型)相當於一個顏料盒子,想把圖片顯示在iPhone設備屏幕上的話首先就是要把散裝顏料裝到這個顏料盒子里。GPUImageFramebuffer就是GPUImage中的顏料盒子,所以要先把UIImage盒子中的顏料倒到GPUImageFramebuffer中,才可以用GPUImage對這些顏料進行再次調色。調色的過程之前的文章中比喻成了一個多維度的管道,因此,在處理過程中就像GPUImageFramebuffer帶著顏料在管子里流動,經過一個節點處理完後會有一個新盒子把調好色的顏料接住再往下流動。
從以上GPUImageFramebuffer.h的內容可看到,主要內容分為三大部分:1.framebuffer及texture的創建。2.GPUImage中GPUImageFramebuffer類型對象的內存管理。3.實際操作過程中數據存儲的CVPixelBufferRef類型對象的具體操作。接下來就來看一下這三點中涉及到的具體類型到底是個啥以及在GPUImage框架中做了哪些事。
網路中對紋理的解釋:
在大學時期有一門計算機圖形課,主要是在Windows上使用C進行OpenGL開發。當時我做了一架直升飛機,雖然具體如何開發OpenGL現在已經有點陌生,但是其中印象非常深刻的有兩個地方:1.只能畫三角形,正方形是通過兩個三角形組成的,圓形是有非常多個三角形組成的,三角形越多,鋸齒越不明顯。2.畫好各種形狀組成三維圖形後可以往圖形上面貼圖,就好像罐頭的包裝一樣,剛做好的罐頭其實只是一個鋁制或者其他材料製成的沒有任何圖案的圓柱體,出產前最後一道工序就是給罐頭貼上一圈紙或者噴上相應的圖案。最後出廠運往各個賣場,我才能買到印有「某巢」以及各個信息在罐子上的奶粉。紋理就是貼在上面的紙或者印在上面的圖案。
紋理的WRAP設置解釋如下(截圖內容來自:http://blog.csdn.net/wangdingqiaoit/article/details/51457675)
字面上的意思是幀緩存,在OpenGL中,幀緩存的實例被叫做FBO。個人理解:如果texture是罐頭上的內容,那framebuffer就是那張裹在罐頭上的紙,它負責對紋理內容進行緩存並渲染到屏幕上,這個過程就叫做render to texture。當然framebuffer中不僅可以緩存原始的紋理內容,還可以是經過OpenGL處理後的內容。比如照片中牛奶罐頭上除了有本身的名字、圖案和說明這些內容,我們看上去還有相應的質感和反光效果,這個就可以通過獲取反光中的實際影像進行透明、拉伸、霧化等效果處理後得到與實際反光一模一樣的圖,最終渲染到牛奶罐上。
從這個角度理解的話,可以把texture看作是framebuffer的一部分,因此在GPUImage中把指向texture的地址作為GPUImageFramebuffer的一個屬性。當然也有不需要創建frambuffer的情況,比如在初始化輸入源時例如GPUImagePicture類型對象,載入圖片資源時只需要創建texture即可,GPUImageFramebuffer的其中一個初始化方法的其中一個參數onlyGenerateTexture為YES時,就可以只創建texture,把圖片信息轉化成紋理,而沒有進行framebuffer的創建。
有創建就有銷毀,GPUImageFramebuffer有一個私有方法- (void)destroyFramebuffer,在dealloc時調用。其中主要的操作是:
相信很多使用過GPUImage都會有同樣的經歷,都會遇到這個斷言。以下就拿具體案例來解釋GPUImage對GPUImageFramebuffer到底做了哪些事情。場景是:創建一個GPUImagePicture對象pic,一個GPUImageHueFilter對象filter和一個GPUImageView對象imageView,實現效果是pic通過filter處理後的效果顯示在imageView上。具體的處理過程就不過多說明,我們來主要看一下GPUImageFramebuffer在整個處理過程中發生的情況。
·GPUImagePicture對象pic中的framebuffer
在對初始化傳入的UIImage對象進行一系列處理後,pic進行自身的framebuffer的獲取:
可以看到,framebuffer對象並不是通過初始化GPUImageFramebuffer來創建的,而是通過調用單例[GPUImageContext sharedFramebufferCache]的其中一個方法得到的。sharedFramebufferCache其實是一個普通的NSObject子類對象,並不具有像NSCache對象對數據緩存的實際處理功能,不過GPUImage通過單例持有這個sharedFramebufferCache對象來對過程中產生的framebuffer進行統一管理。獲取framebuffer的方法有兩個入參:1.紋理大小,在pic中的這個大小正常情況下是傳入圖片的初始大小,當然還有不正常情況之後再說。2.是否返回只有texture的的framebuffer對象。
第一步:
傳入的紋理大小,textureOptions(默認)、是否只要texture調用sharedFramebufferCache的- (NSString *)hashForSize:(CGSize)size textureOptions:(GPUTextureOptions)textureOptions onlyTexture:(BOOL)onlyTexture;方法得到一個用於查詢的唯一值 lookupHash。 由此可見如果紋理大小一致的GPUImagePicture對象獲取到的lookupHash是相同的。
第二步:
lookupHash作為key,在sharedFramebufferCache的一個字典類型的屬性framebufferTypeCounts獲取到 ,如字面意思,在緩存中滿足條件的GPUImageFramebuffer類型對象的個數。轉換為整數類型為 numberOfMatchingTextures 。
第三步:
如果 小於1也就是沒找到的話,就調用GPUImageFramebuffer的初始化方法創建一個新的framebuffer對象。否則:將lookupHash和(numberOfMatchingTextures - 1)拼接成key從sharedFramebufferCache的另一個字典類型的屬性framebufferCache中獲取到framebuffer對象。再更新framebufferTypeCounts中數值(減1) 。
第四步:
以防最後返回的framebuffer對象為nil,最後做了判斷如果為nil的話就初始化創建一個。
第五步:
調用作為返回值的framebuffer對象的- (void)lock;方法。主要是對 framebufferReferenceCount 進行+1操作。framebufferReferenceCount為framebuffer的一個屬性,初始化時候為0,作用也是字面意思:對象被引用的次數。此時需要進行+1操作是因為framebuffer對象即將被獲取它的pic引用並將圖片內容載入。
但是!與GPUImageFilter類型對象不同的是在pic獲取到framebuffer後,進行了[outputFramebuffer disableReferenceCounting];。這個方法里將framebuffer的 referenceCountingDisabled 設置為YES。而這個屬性的值在- (void)lock;方法中又會導致不一樣的結果。如果referenceCountingDisabled為YES的話將不會對framebufferReferenceCount進行+1操作。
然而問題就出在這里 ,在pic獲取到framebuffer之前,從sharedFramebufferCache找到framebuffer後就對它調用了- (void)lock;方法,此時pic獲取到的framebuffer對象的framebufferReferenceCount已經被+1,而referenceCountingDisabled是在這之後才設置為YES,從而導致在pic對象dealloc時候自身的outputFramebuffer屬性並未得到釋放從而引起內存泄漏。為了解決這個問題我寫了一個GPUImagePicture的caterogy,重寫dealloc方法,將原本的[outputFramebuffer unlock];替換成 [outputFramebuffer clearAllLocks];, 保證outputFramebuffer的framebufferReferenceCount被重置為0,從而保證outputFramebuffer能順利釋放。
·GPUImageHueFilter對象filter中的framebuffer
我們以結構最簡單的單輸入源濾鏡對象作為例子,通過GPUImageHueFilter對象filter。filter相比pic來說相同點是:自身都會通過sharedFramebufferCache獲取到一個framebuffer用來存儲經過自身處理後的數據並傳遞給下一個對象。不同點是:filter有一個 firstInputFramebuffer 變數,作用是引用上一個節點的outputFramebuffer。如果是繼承自GPUImageTwoInputFilter的濾鏡對象來說,它的成員變數將會多一個secondInputFramebuffer。若想進行到filter這,必須將filter作為pic的target,並調用pic的processImage方法。filter中方法調用順序是:
1. 對輸入的framebuffer引用並調用lock方法。假設pic的framebuffer為初始化創建的,傳入前framebufferReferenceCount為1,經過此方法後framebufferReferenceCount則為2
2. filter的處理操作,也就是framebuffer的渲染紋理。這里就復雜了,那就不涉及到具體的渲染過程了。
filter的outputFramebuffer與pic一樣,都是通過shareFramebufferCache查找獲得。因此,outputFramebuffer變數被賦值時framebuffer的framebufferReferenceCount已經為1。接下來有一個判斷條件: usingNextFrameForImageCapture 。在類中全局搜一下,發現在調用 - (void)useNextFrameForImageCapture; 方法時會將usingNextFrameForImageCapture設置為YES。那麼什麼時候會調用這個方法呢?有用GPUImage寫過最簡單的離屏渲染功能實現的都會覺得對這個方法有點眼熟,那就是在filter處理完後導出圖片前必須要調用這個方法。為啥呢?原因就在這個判斷條件這里,如果usingNextFrameForImageCapture為YES,那麼outputFramebuffer要再lock一次,就是為了保證在處理完成後還需要引用outputFramebuffer,從而才可以從中生成圖片對象,否則就被回收進shareFramebufferCache啦。
進行過一頓操作後,最後會調用輸入源的unlock方法。這時候firstInputFramebuffer的framebufferReferenceCount按照正常情況的話將會為0,就會被添加到shareFramebufferCache中。
3. 接下來執行的方法中會把自身的outputFramebuffer傳遞給鏈中的下一個節點,就像pic到filter的過程一樣。
這里的[self framebufferForOutput]在正常的filter中會返回outputFramebuffer。如果usingNextFrameForImageCapture為YES的話,可以簡單理解當前對象的outputFramebuffer傳遞給下一個target後還有其他用處的話就不置空outputFramebuffer變數。如果usingNextFrameForImageCapture為NO的話,當前outputFramebuffer被置為nil,但是原先outputFramebuffer指向的framebuffer並不會被回收到shareFramebufferCache。原因是:framebuffer已經傳遞給下個target,在相應賦值方法中對framebuffer調用了lock方法。周而復始,直到最後一個節點,要麼生成圖片,要麼顯示。
C. 幀緩存的定義
一個支持OpenGL渲染的窗口 (即幀緩存) 可能包含以下的組合:
· 至多4個顏色緩存
· 一個深度緩存
· 一個模板緩存
· 一個積累緩存
· 一個多重采樣緩存