美文网首页iOS技术交流收藏视频好东西
GPUImage(OpenGL ES)的性能优化、爬坑与架构改善

GPUImage(OpenGL ES)的性能优化、爬坑与架构改善

作者: 熊皮皮 | 来源:发表于2017-08-23 00:44 被阅读1519次

    本文档主要描述了Objective-C版GPUImage的代码优化,主要处理因不规范调用OpenGL ES接口所引起的性能问题,同时简要介绍Shader代码的优化思路。另外,分享一些自己开发过程所遇OpenGL ES问题的解决办法。

    结论:
    (1)优化前,使用双边滤波、美白等滤镜,对于iPhone 7p(10.3.3)每帧GPU总耗时主要在[17, 18]毫秒区间波动。
    (2)优化后,每帧GPU总耗时主要在[9, 18]毫秒区间波动,一般稳定在[12, 14]毫秒。

    无论OpenGL ES、Metal还是Vulkan,优化都是两块:CPU和GPU。目标是减少CPU调用、降低I/O以及简化Shader代码中的复杂逻辑。

    性能优化

    1、GPU

    1.1、I/O

    很多人的第一想法是,数据从CPU拷贝到GPU后,在GPU内部随意拷贝的成本很低。事实上,对于PowerVR这种TBDR技术的GPU而言,这个成本依旧很高。比如,720P的纹理,即便是简单地将输入图像拷贝到输出纹理也有不小的开销,在iPhone 7p上大约是1~2毫秒。实际上,对于PowerVR的GPU,做计算时数据是从统一内存(Unified Memory,CPU和GPU共同管理的内存)拷贝到GPU自己的贴片内存(Tile Memory),这个内存很小,我理解是类似CPU的Cache,如果Fragment Shader里经常要读取不同坐标的纹理,那么Tile Memory的缓存就会失效,此时要重新读取Unified Memory的数据,这样比正常情况花更多时间才能完成操作。

    因此,减少不必要的I/O和绘制是非常有必要的。特别地,有些算法的优化时就使用了这个伎俩。

    1.2、Shader优化

    Shader代码实现了我们的算法,想要大幅降低Shader的耗时,降低图像质量实现减少计算量是最有效的办法。在尽量可能保证质量的基础上进行优化,一般有这些办法:
    (1)避免在Shader中使用循环或分支判断语句
    (2)避免依赖纹理读取。即,避免在Fragment Shader计算纹理坐标,将计算提前到Vertex Shader,减少计算次数。
    (3)避免交换纹理坐标分量。这会造成依赖纹理读取。
    (4)避免在Fragment Shader做pow等数据计算。同样的,尽可能将计算提前到Vertex Shader,减少计算次数
    (5)使用更少的颜色分量参与计算。选择影响结果的主要颜色分量参与计算,这也是减少计算量的有效方法。
    (6)降低数据精度。比如,从Vertex Shader传递到Fragment Shader的纹理坐标精度从highp改成mediump也会降低一些消耗。

    2、CPU

    2.1、纹理相关警告

    2.1.1、基于CVPixelBuffer创建纹理的问题

    具体表现为两个警告,如下左图所示:

    1. Framebuffer color attachment internal format is not texture-filterable.
    2. Missing framebuffer attachments
    Framebuffer的优化对比

    第一个警告出现的原因是GPUImageFramebuffer在generateFramebuffer实现时若当前iOS系统版本支持CVOpenGLESTextureCacheCreate接口则每次都基于CVPixelBuffer创建纹理,这么做的好处是读取渲染结果不需要调用glReadPixels,避免了阻塞当前线程及拷贝GPU数据到CPU的I/O消耗。实际开发中,一般只有最后一个滤镜的处理结果才有从GPU取回CPU的需求,前面的滤镜并不需要CVPixelBuffer的支持,可用glTexImage2D优化。优化的结果如上右图所示。

    2.1.2、glTexImage2D创建空白纹理的问题

    glTexImage2D创建空白的纹理存储空间,一般将最后一个参数pixels设置为NULL。这会带来另一个问题:

    Uninitialized texture data

    创建空texture的警告

    优化这个警告只需要提供空白的初始化数据。副作用是,触发CPU向GPU拷贝数据,而Xcode认为这个拷贝无意义数据的行为是对的

    2.2、没使用Vertex Buffer Object(VBO)的问题

    Vertex array not contained in buffer object

    没使用Vertex Buffer Object(VBO)带来的警告

    GPUImage的实现里没使用VBO,当多个Filter叠加时,每个Filter绘制都需要上传坐标数据会带来额外的消耗。对GPUImage进行二次开发,在GPUImageFilter中创建VAO及VBO,对于某一Filter需要旋转图像,可在其自己的着色器中使用旋转矩阵达到目的。

    新增:
    1)落影在评论中写道有些滤镜会改变顶点坐标。我觉得,这个场景应该由此滤镜维护自己的VBO,父类提供公用的VBO,没特殊需求的滤镜直接用父类的VBO即可。

    2.3、Logical Buffer Load与Logical Buffer Store

    对于iPhone等使用Tile Based Deferred Rendering技术的GPU设备而言,从统一内存到GPU的tile内存之间拷贝数据是非常耗时的。无论是Logical Buffer Load还是Logical Buffer Store,都应该尽量避免,从而节约I/O时间,特别是处理高分辨率纹理的场合。

    2.3.1、Logical Buffer Load

    什么是Logical Buffer Load?

    Loading a tile is called a Logical Buffer Load.

    Slow Framebuffer Load也描述了类似的行为,Slow Framebuffer Load可能是新版本Xcode的说法

    Logical Buffer Load的行为如下两图所示。

    Logical Buffer Load加载Render Buffer的数据 Logical Buffer Load加载Depth Buffer的数据

    在每次绘制前调用glClear可解决Logical Buffer Load。

    2.3.2、Logical Buffer Store

    什么是Logical Buffer Store?

    Storing tile to unified memory called Logical Buffer Store

    调用(OpenGL ES 2.0)glDiscardFramebufferEXT或(OpenGL ES 3.0)glDiscardFramebuffer解决Logical Buffer Store。示例代码如下所示:

    const GLenum discards[] = {GL_DEPTH_ATTACHMENT, GL_COLOR_ATTACHMENT0};
    glDiscardFramebufferEXT(GL_FRAMEBUFFER, sizeof(discards)/sizeof(discards[0]), discards);
    

    2.4、Inefficient state update

    在状态不变的情况下,每帧都调用glEnableVertexAttribArrayglActiveTexture等函数也是额外的开销。OpenGL (ES)是基于状态机实现的,即使我们将状态设置为相同的值,也会有开销。GPUImage没实现额外的GPU状态管理,这是个小小的遗憾。

    解决的办法是,自己实现状态管理,所有改变状态的操作都汇集到此状态机上,最后调用glDraw*时才发送实际需要变更的状态给OpenGL。

    2.5、The same uniform location has been queried more than once

    这实际是我们二次开发时没按GPUImage约定所带来的问题。GPUImage约定在init时编译链接program,同时获取相应的顶点及统一变量的位置,在renderToTextureWithVertices:textureCoordinates:进行实际绘制所需的操作,而无需每次绘制时获取这些变量的位置。

    2.6、避免在不需要的场合启用glBlend

    混合对性能的影响很大,可通过GPU Overrides->Disable blending stage对比不使用混合的性能表现。在我的测试中,每帧18毫秒GPU耗时,使用Disable blending stage后能降到12毫秒左右。

    2.7、避免频繁调用glFlush

    所谓频繁调用glFlush,指的是每次glDrawXXX后立即调用glFlush

    举例:渲染到中间纹理时,glDraw*后面马上绑定默认帧缓冲区,然后调用glFlush会触发Logical Buffer Store。此时由于绑定了默认帧缓冲区,按3.2节介绍的glDiscardFramebufferEXT可能会出现GL_INVALID_ENUM错误,原因是默认帧缓冲区没绑定renderbuffer作为其GL_COLOR_ATTACHMENT0。

    一般在共享E(A)GLContext的线程上传纹理时才需要glFlush同步给绘制线程,单个GL上下文场合尽量避免调用glFlush。对于多线程共享GL上下文的场合,资源加载线程调用glFlush或glFinish同步给绘制线程是避免不了的。

    对于每次glDrawXXX后立即调用glFlush的出发点是,由于GL命令队列要么填充满,要么主动调用glFlushglFinish函数才会从CPU传递指令到GPU,而这段时间GPU可能处于空闲状态。如果能及时让操作系统将GL命令调用传递给GPU,那就始终让GPU处于忙碌状态。这种说法,我持保留意见。

    2.8、“无状态绘制”

    这是个有趣的说法。OpenGL(ES)基于状态机管理各种绘制操作,不可能有“无状态绘制”的实现。

    为了实现“无状态绘制”,每次调用glDrawXXX,都将本次绘图调用所设置的状态重置为默认状态。优势是不会因为当次绘制设置了特殊的状态却忘了关闭,影响后续的绘制结果。缺点是,由于多数状态是可与后续绘制共享,每次都暴力恢复状态,加大了CPU开销。

    爬坑

    3、OpenGL ES异常问题与处理

    3.1、非2的n次方纹理使用GL_REPEAT

    这是腾讯的宇哥遇到的问题。他创建一个300x300的纹理并将纹理坐标状态设置为

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    

    在iOS上绘制出来的纹理为黑色。这是因为300并非2的n次方。而GL_REPEAT在OpenGL ES标准用法是要求纹理尺寸为2的n次方。khronos标准文档说明如下:

    Similarly, if the width or height of a texture image are not powers of two and either the GL_TEXTURE_MIN_FILTER is set to one of the functions that requires mipmaps or the GL_TEXTURE_WRAP_S or GL_TEXTURE_WRAP_T is not set to GL_CLAMP_TO_EDGE, then the texture image unit will return (R, G, B, A) = (0, 0, 0, 1).

    return (R, G, B, A) = (0, 0, 0, 1)就是黑色。

    3.2、在Lookup滤镜中错误打开各向异性纹理过滤

    在3维渲染场合,当相机观察方向与模型表面不垂直导致远处纹理细节丢失、画面模糊。启用各向异性纹理过滤(GL_EXT_texture_filter_anisotropic)可以改善这个问题。

    然而,对于GPUImageLookupFilter这种映射颜色查找表的操作而言,GL_TEXTURE_MAX_ANISOTROPY_EXT让图像出现噪点。而且,单独设置gl_FragColor每个通道的值也失败。这个问题花了不少时间定位,具体原因是渲染引擎将所有纹理都设置了GL_TEXTURE_MAX_ANISOTROPY_EXT。正常情况下GL_TEXTURE_MAX_ANISOTROPY_EXT值为1,如果使用如下代码,那在iPhone 7上是9729。

    GLfloat fLargest;
    glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &fLargest);
    // ...
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, fLargest);
    

    目前的结论是,2维渲染场景应该避免设置GL_TEXTURE_MAX_ANISOTROPY_EXT。

    架构改善

    由于最近在写跨平台版本GPUImage,同时加入了很多新特性,这里只是之前的简单。后续会重写本节

    目标管理

    GPUImage添加目标(消费者)时会设置当前操作的输出为目标的输入,源码如下所示。注意语句[self setInputFramebufferForTarget:newTarget atIndex:textureLocation];

    - (void)addTarget:(id<GPUImageInput>)newTarget atTextureLocation:(NSInteger)textureLocation;
    {
        if([targets containsObject:newTarget])
        {
            return;
        }
        
        cachedMaximumOutputSize = CGSizeZero;
        runSynchronouslyOnVideoProcessingQueue(^{
            [self setInputFramebufferForTarget:newTarget atIndex:textureLocation];
            [targets addObject:newTarget];
            [targetTextureIndices addObject:[NSNumber numberWithInteger:textureLocation]];
            
            allTargetsWantMonochromeData = allTargetsWantMonochromeData && [newTarget wantsMonochromeInput];
        });
    }
    

    实际上,这里的设置setInputFramebufferForTarget:atIndex:意义不大,而且删掉更合理。个人分析的原因是,为了通知target记录已添加的InputFramebuffer位置才在添加目标时就调用此方法。另一个考虑是,照顾GPUImagePicture这类“特殊”操作:允许先调用processImageaddTarget:。示例代码如下所示。

    [imageForBlending processImage];
    [imageForBlending addTarget:currentlySelectedFilter];
    

    更合理的做法是,翻转上面操作顺序,应该先addTarget,再processImage。与其他操作(Filter)用法保持一致。同时,在addTarget:atTextureLocation:时让目标记录下已添加的输入位置。因为informTargetsAboutNewFrameAtTime:会将自身的输出设置为目标的输入,这完全够用。结论是,这个实现与原实现的区别是没设置自己的outputFramebuffer为目标的inputFramebuffer,因为此时outputFramebuffer是nil。意义不大。

    合并多输入子类GPUImageXYXInputFilter

    为了支持多个纹理输入,GPUImage实现了多种InputFilter,比如GPUImageTwoInputFilter、GPUImageThreeInputFilter等等。这样的好处是,降低开发难度。根据业务需求,方便添加更多输入的子类。缺点也是显而易见的:代码冗余。

    由于每添加一个输入纹理(GPUImageFramebuffer)就得添加一组相当的状态,将所有状态以列表的方式表示,每个操作(Filter)初始化时通知基类自己支持的输入纹理个数,基类负责初始化合适的状态组。那么,GPUImageTwoInputFilter、GPUImageThreeInputFilter等可用比如maxInputNumber的值表示。

    相关文章

      网友评论

      • dkStart:你好,我想摄像头打开的时候,实时识别人脸(coreImage),然后对人脸添加滤镜,请问能提供一个思路吗,或者说需要了解什么技术
      • 2f4c5c39000b:GL剔除背面是否也可以提高性能?
        熊皮皮:@邪恶的西瓜 可以
      • 落影loyinglin:不同滤镜的顶点不一定是全屏,就几个顶点数据,这个量级耗时也很长吗。并不是复杂的多顶点
        熊皮皮:@落影loyinglin 我没看到你说的不同顶点,如果是这样,那就由滤镜管理自己和VBO。顶点着色器的执行耗时短,但是拷贝数据占了多余的命令缓冲区空间。

      本文标题:GPUImage(OpenGL ES)的性能优化、爬坑与架构改善

      本文链接:https://www.haomeiwen.com/subject/jeerdxtx.html