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个颜色缓存
· 一个深度缓存
· 一个模板缓存
· 一个积累缓存
· 一个多重采样缓存