神刀安全网

iOS Video Toolbox:GPGPU加速YUV图像处理

本文档以调整YUV亮度为例,描述了OpenGL ES 3.0 Transform Feedback(变换反馈)在Video Toolbox解码回调中进行YUV图像处理的具体实现,同时比较多种绘制模式之间的性能差异,此文档目标是利用GPU并行计算能力,做通用计算(比如浮点计算),即是当成GPGPU使用,减轻非游戏及GPU密集型应用的CPU压力,从而导出一个通用计算骨架用于各类有计算性能需要的场合,是文档iOS GPGPU 编程:Transform Feedback实现图像对比度调整iOS Video Toolbox:读写解码回调函数CVImageBufferRef的YUV图像的拓展。

总结:
1、在iPhone 7上实现GPGPU(基于Transform Feedback),性能比iPhone 6有近10倍提升。
2、对于1080p点阵(两多百万个点),在iPhone 6或同等运算力设备上进行逐点计算在只使用顶点着色器(Vertex Feedback)进行单通滤波(Single Pass)场合是难以达到实时处理的。
3、根据WWDC: 602 Adopting Metal Part 1的描述,OpenGL ES的接口在iOS(猜测是iOS 10)上由Metal接口封装实现,故直接使用Metal少了封装层次,理论上有更高的性能表现。
4、实例化渲染(Instancing Rendering)的朴素实现对于1920×1080点阵的绘制并没提升性能,但是显著节省了GPU存储空间。

目录:
|- 同步解码与CPU实现YUV亮度减半
|- 同步解码与GPU逐点绘制实现YUV亮度减半
|– Transform Feedback逐点计算
|– 逐点绘制的性能分析
|- 同步解码与GPU逐行绘制实现YUV亮度减半
|- 同步解码与GPU逐三角形绘制实现YUV亮度减半
|- 同步解码与GPU用实例化渲染(Instancing Rendering)逐三角形绘制实现YUV亮度减半
|– 基于gl_InstanceID的Instancing Rendering实现
|– 基于glVertexAttribDivisor的Instancing Rendering实现
|- 性能优化
|– 克服气泡效应(CPU等待GPU完成)
|– 未初始化颜色附着
|– 未初始化纹理数据
|– 逻辑缓冲区存储
|– 逻辑缓冲区加载
|– 冗余调用
|- 总结
|- 致谢
|- 参考与推荐阅读

此文档在编写过程中,遇到一个绘制模式问题且在StackOverflow OpenGL ES 3.0 Transform Feedback glDrawArrays with GL_LINE_STRIP output is all zeros进行求助,感兴趣的同学请前往回答。

除非特别标注,此文档数据皆来自iPad Air 2(iOS 9.3.4)真机测试,测试视频数据为1920×1080@24FPS、时长181.44秒。正式开始前,先给Xcode 8 GPU调试新增的小功能点赞。

iOS Video Toolbox:GPGPU加速YUV图像处理
Xcode 8 GPU调试新增功能:查看缓冲区数据大小

1、同步解码与CPU实现YUV亮度减半

为比较GPU性能,先给出CPU同步解码且在解码回调函数中实现调整YUV亮度操作,朴素实现如代码段1所示,代码中的除法操作只是出于演示目的,整除2换成移位操作按理会对性能有所改善,但是,如果除以非2的幂则无法使用移位操作了,感谢@Bepartofyou指出此示例的不适当问题。我原本是想比较CPU做浮点操作与GPU的性能差异,只是恰巧Y通道是8位整数值。

// 代码段1-1:同步解码 VTDecodeFrameFlags decodeFlags = 0; VTDecodeInfoFlags outFlags = 0; VTDecompressionSessionDecodeFrame(decompressionSession,      sampleBufferRef,      decodeFlags,      NULL,      &outFlags); // 代码段1-2:解码回调 void decoderCallback(void *decompressionOutputRefCon,                      void *sourceFrameRefCon,                      OSStatus status,                      VTDecodeInfoFlags infoFlags,                      CVImageBufferRef imageBuffer,                      CMTime presentationTimeStamp,                      CMTime presentationDuration ) {     if (noErr != status) {         return;     }     if (!imageBuffer) {         return;     }     CVPixelBufferLockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly);     void *lumaPlane = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0);     int width = CVPixelBufferGetWidthOfPlane(imageBuffer, 0);     int height = CVPixelBufferGetHeightOfPlane(imageBuffer, 0);     for (int line = 0; line < height; ++line) {         for (int col = 0; col < width; ++col) {             currentLumaPlane[col] /= 2;         }         currentLumaPlane += width;     }     CVPixelBufferUnlockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly); }

在iPhone 5(iOS 9.2)上运行了2055.8秒,平均每帧消耗0.47秒。

iOS Video Toolbox:GPGPU加速YUV图像处理
iPhone 5(iOS 9.2)CPU做Y通道亮度减半的资源消耗

在iPhone 6p(iOS 9.3.5)上运行了95.7秒,平均每帧消耗0.02秒,可见两年后的设备性能确实是大幅提升。

iOS Video Toolbox:GPGPU加速YUV图像处理
iPhone 6p(iOS 9.3.5)CPU做Y通道亮度减半的资源消耗

iPad Air 2与iPhone 6p表现接近。

iOS Video Toolbox:GPGPU加速YUV图像处理
iPad Ari 2(iOS 9.3.4)CPU做Y通道亮度减半的资源消耗

再对比iPhone 7的表现,相对iPad Air 2提高了10秒左右。

iOS Video Toolbox:GPGPU加速YUV图像处理
iPhone 7(iOS 10.0.1)CPU做Y通道亮度减半的资源消耗

2、同步解码与GPU逐点绘制实现YUV亮度减半

因iOS平台限制,Transform Feedback必须使用GLKit,故解码回调函数的实现代码变得略微复杂。

2.1、Transform Feedback逐点计算

变换反馈也称为顶点反馈。使用顶点反馈的具体表现是,在同步进行CPU和GPU处理时,每次创建纹理都需要占用主线程资源且为了让CPU及时获取GPU处理数据还强制了CPU等待GPU命令队列完成,失去了GPU的异步执行特性,造成汽泡效应,如代码段2所示。当然,这种实现是非常不合理的,比如,理论上使用共享组(Share Group),纹理的创建可以在子线程实现。

// 代码段2-1:通知主线程 dispatch_sync(dispatch_get_main_queue(), ^{     // 通知主线程绘制 }); // 代码段2-2:Y通道创建纹理 - (void)generateTextureFromCVPixelBuffer:(CVPixelBufferRef)pixelBuffer {     [lock lock];     isNewPixelBuffer = true;     CVOpenGLESTextureCacheFlush(textureCache, 0);     if (texture) {         CFRelease(texture);     }     int imageWidth = (int)CVPixelBufferGetWidthOfPlane(pixelBuffer, 0);     int imageHeight = (int)CVPixelBufferGetHeightOfPlane(pixelBuffer, 0);     CVReturn createResult = CVOpenGLESTextureCacheCreateTextureFromImage(NULL,         textureCache,         pixelBuffer,         NULL,         GL_TEXTURE_2D,         GL_LUMINANCE,         imageWidth, imageHeight,         GL_LUMINANCE,         GL_UNSIGNED_BYTE,         0,         &texture);     if (kCVReturnSuccess != createResult) {         NSLog(@"CVOpenGLESTextureCacheCreateTextureFromImage failed.");     }     glBindTexture(CVOpenGLESTextureGetTarget(texture),          CVOpenGLESTextureGetName(texture));     glTexParameteri(GL_TEXTURE_2D,          GL_TEXTURE_WRAP_S,          GL_CLAMP_TO_EDGE);     glTexParameteri(GL_TEXTURE_2D,          GL_TEXTURE_WRAP_T,          GL_CLAMP_TO_EDGE);     glTexParameteri(GL_TEXTURE_2D,          GL_TEXTURE_MIN_FILTER,          GL_NEAREST);     glTexParameteri(GL_TEXTURE_2D,          GL_TEXTURE_MAG_FILTER,          GL_NEAREST);     [lock unlock]; } // 代码段2-3:GPU处理Y通道数据 // 进行变换反馈,CPU等待GPU运行结束,获取GPU内存且进行跨距处理 GLuint *addr = glMapBufferRange(GL_TRANSFORM_FEEDBACK_BUFFER,      0,      pointsCount * sizeof(GLuint),      GL_MAP_READ_BIT); GLuint *pAddr = addr; if (addr) {     uint8_t *luma = calloc(pointsCount, sizeof(uint8_t)), *pLuma = luma;     for (int line = 0; line < 1080; ++line) {         for (int col = 0; col < 1920; ++col) {             luma[0] = addr[0];             ++luma;             ++addr;         }     }     luma = pLuma;     addr = pAddr;     // 处理luma数据     free(luma); } glUnmapBuffer(GL_TRANSFORM_FEEDBACK_BUFFER);

现在,真机运行查看性能表现。

iOS Video Toolbox:GPGPU加速YUV图像处理
iPad Air 2使用顶点着色器1920×1080画点实现Y通道亮度减半的CPU资源消耗

iOS Video Toolbox:GPGPU加速YUV图像处理
iPad Air 2 CPU时间消耗测试概览

可见,CPU压力几乎全在主线程,但负荷较低,其余核心几乎处于休眠状态,主线程多次出现尖峰可能由创建并上传纹理的代码引起,后续文档会对此进行优化。下面观察GPU的资源占用。

iOS Video Toolbox:GPGPU加速YUV图像处理
iPad Air 2 使用顶点着色器1920×1080画点实现Y通道亮度减半的GPU资源消耗

苹果使用PowerVR的图形处理器,而PowerVR处理顶点是基于Tile(贴片)方式实现的,那么上图反应了我们代码确实只有顶点着色器代码在工作,因为PowerVR处理片段着色器对应于RENDER功能。DEVICE图表则表示了GPU整体使用,包含顶点与片段着色器的代码消耗。有关Utilization图表的说明如下所示。

Utilization. Shows three bars, breaking down your app’s use of the different processing resources on the GPU and indicating the possible locations of performance bottlenecks in your use of graphics hardware.
The Tiler bar measures use of the GPU’s geometry processing resources. High tiler utilization can indicate performance bottlenecks in the vertex and primitive processing stages of the OpenGL ES pipeline, such as using inefficient vertex shader code or drawing an excessive number of vertices or primitives each frame.

The Renderer bar measures use of the GPU’s pixel processing resources. High renderer utilization can indicate performance bottlenecks in the fragment and pixel processing stages of the OpenGL ES pipeline, such as using inefficient fragment shader code or processing additional fragments each frame for color blending.

The Device bar shows overall GPU usage, incorporating both tiler and renderer usage.

有关GPU相关工具说明可官方文档Xcode OpenGL ES Tools Overview
另外,WWDC: Advances in OpenGL ES 3.0对A7处理器有如下说明。由于没直接说明A8、A9及A10等处理器与A7的关系,在此只是猜测A8X还使用TBDR技术。

iOS Video Toolbox:GPGPU加速YUV图像处理
WWDC对A7处理器的说明(基于贴片延迟渲染技术)

虽然当前代码占用了较多CPU和GPU时间,从CPU核心使用率上看,至少,我们的初步目标实现了:在非游戏和图形密集计算的应用中,使用GPU计算、释放CPU压力。接下来,进行性能测试,确定此方式是否达到实际产品需求并进行性能瓶颈定位。

2.2、逐点绘制的性能分析

在Instruments的帮助下,本节进行性能分析,当前实现方式存在不少问题,如下图所示。在只改变绘制模式代码,则GL_POINTS、GL_LINES和GL_TRIANGLES存在相同性能问题,在此只给出绘点的性能相关图表,所有性能相关问题另起章节讨论。

iOS Video Toolbox:GPGPU加速YUV图像处理
iPad Air 2 GPU绘点实现存在的问题

iOS Video Toolbox:GPGPU加速YUV图像处理
iPad Air 2 GPU绘点性能分析(接口调用统计)

iOS Video Toolbox:GPGPU加速YUV图像处理
iPad Air 2 GPU 绘点性能分析(帧统计)

3、同步解码与GPU逐行绘制实现YUV亮度减半

使用上一节生成的1920×1080个点坐标,只修改绘制参数,从绘点GL_POINTS到画线GL_LINES后,帧率提高了一倍,达到每秒6帧,时间减半,不过,CPU占用略有提高,平均为9%。从这一表现,可推断,将绘制方式改成三角形网格(Mesh),那么,帧率将继续提高。
由于点坐标没重新生成,意味着,(0, 0)(0, 1)连成线、(0, 2)(0, 3)连成线,而(0, 1)(0, 2)两点则未结成线,以此类推。然而,绘制的结果画面是完整的,参考退化三角形的概念,我的理解是,GPU可能拒绝了这种绘制直线的方式,从而并没画线,只是一次读取取两个端点,加快了顶点读取速度,然后只是简单地画成点阵。之前我根据印象,说可能形成『退化直线』,即长度0,因为在线渲染(Live Rendering,最终结果输出到默认帧缓冲区)的场合,光栅化的结果中最小绘制单位是像素点,而此处两个点之间再无间隙可供插值或填充。这种理解在重新查阅资料后,发现是错误的。退化三角形的定义是判断顶点是否重合,而非三角形面积是否为0,完整定义如下所示。

退化三角形是两个或更多顶点相同的三角形。GPU可非常简单地检测和拒绝退化三角形,所以这是很好的性能改进,可将一个很大的图元放入由GPU渲染的队列。
摘自《OpenGL ES 3.0编程指南(原书第2版)》7.2.4 性能提示

iOS Video Toolbox:GPGPU加速YUV图像处理
iPad Air 2 使用顶点着色器1920×1080画线实现Y通道亮度减半的GPU资源消耗

iOS Video Toolbox:GPGPU加速YUV图像处理
iPad Air 2 使用顶点着色器1920×1080画线实现Y通道亮度减半的CPU资源消耗

4、同步解码与GPU逐三角形绘制实现YUV亮度减半

本节验证了上一节同步解码与GPU逐行绘制实现YUV亮度减半的推论。使用第一节生成的点阵绘制独立三角形,帧率从6升到8帧每秒,比预想值少了一帧,总耗时及每帧平均消耗的CPU与GPU时间有所改善,然而,CPU占用率从平均9%涨到平均12%,详细资源占用信息如下。

iOS Video Toolbox:GPGPU加速YUV图像处理
iPad Ari 2(iOS 9.3.4)使用顶点着色器1920×1080画独立三角形实现Y通道亮度减半的CPU资源消耗

iOS Video Toolbox:GPGPU加速YUV图像处理
iPad Ari 2(iOS 9.3.4)使用顶点着色器1920×1080画独立三角形实现Y通道亮度减半的GPU资源消耗

5、同步解码与GPU用实例化渲染(Instancing Rendering)逐三角形绘制实现YUV亮度减半

在上一节绘制独立三角形的基础上,使用OpenGL ES 3.0新增特性实例化渲染(Instancing Rendering)实现1920×1080点阵处理,查看Instancing Rendering是否适用GPGPU场合。其实,在顶点着色器中做纹理采样也是ES 3.0的新特性Instancing Rendering的特点是一次绘图调用可绘制多个相似的物体,并且这些物体可以拥有不同属性,比如位置、颜色、方向等。在绘制人群、森林等存在大量相似物体的场景,它可避免多次glDraw*调用,节省CPU时钟周期。具体应用示例如下图所示,图是偷WWDC的,我承认。

iOS Video Toolbox:GPGPU加速YUV图像处理
Instancing Rendering绘制多个相似物体示例

因实例化渲染(Instancing Rendering)存在两种用法,故下面分两小节进行描述。

5.1、基于gl_InstanceID的Instancing Rendering实现

顶点着色器读取32位整数gl_InstanceID与原纹理坐标相加再采样纹理,示例代码如下所示。

vec2 normalized_texcoord = vec2(vin_position.x / u_image_width,      (vin_position.y + float(gl_InstanceID))/ u_image_height);

根据WWDC的介绍,对于片段着色器而言,修改纹理坐标是非常影响性能的。推断,对于顶点坐标也是如此。为了适配新的绘制方法,还需使用glDrawArraysInstanced替换glDrawArrays,这个函数的定义如下所示。

void glDrawArraysInstanced(    GLenum mode,      GLint first,      GLsizei count,      GLsizei primcount);

前三个参数与glDrawArrays的定义相同,最后一个参数primcount表明要绘制的物体数量。故替换旧绘制函数为:

glDrawArraysInstanced(GL_TRIANGLES,      0,      1920/* 点数 */,      1080 /* 实例数,要绘制1080行 */);

现在,只需一行的点阵数据就可处理1920×1080的图像了,节约了大量存储空间。对比本文开始的截图,Array Buffer和TF Buffer分别从15.8MB、15.8MB减少到15KB和7.9MB。GPU帧率从7提高到8,不过,CPU和GPU耗时基本没改善

iOS Video Toolbox:GPGPU加速YUV图像处理
iPad Ari 2(iOS 9.3.4)GPU用InstancingRendering绘制独立三角形的GPU缓冲区占用情况

iOS Video Toolbox:GPGPU加速YUV图像处理
iPad Ari 2(iOS 9.3.4)GPU用InstancingRendering(gl_InstancingID)绘制独立三角形的GPU资源消耗

5.2、基于glVertexAttribDivisor的Instancing Rendering实现

基于glVertexAttribDivisor函数的实现需要的代码略多些,之所以需要两种方式都实现,是因为WWDC讲座建议我们进行真机实测,选择性能更好的实现方式。现在调整代码以适配此实现方式。

// 顶点着色器 layout (location = 1) in float vin_current_row; // 生成实例相关顶点数据 for (int row = 0; line < imageSize.height; ++row) {     lines[row] = row; } // 上传实例相关顶点数据 GLuint gpuLineInputBuffer; glGenBuffers(1, &gpuLineInputBuffer); glBindBuffer(GL_ARRAY_BUFFER, gpuLineInputBuffer); glBufferData(GL_ARRAY_BUFFER,      sizeof(lines),      lines,      GL_STATIC_DRAW);  glVertexAttribPointer(1,      1,      GL_FLOAT,      GL_FALSE,      0,      NULL); glEnableVertexAttribArray(1); glVertexAttribDivisor(1, 1); // 绘图调用 glDrawArraysInstanced(GL_TRIANGLES,      0,      1920/* 点数 */,      1080 /* 实例数,要绘制1080行 */);

从下面运行截图看,glVertexAttribDivisor实现方式的GPU缓冲区占用情况与gl_InstancingID一样。帧率在7和8之间变化,多数情况在7帧率,CPU耗时基本一致。

iOS Video Toolbox:GPGPU加速YUV图像处理
iPad Ari 2(iOS 9.3.4)GPU用InstancingRendering(Disivor)绘制独立三角形的GPU资源消耗

iOS Video Toolbox:GPGPU加速YUV图像处理
iPad Ari 2(iOS 9.3.4)GPU用InstancingRendering(Disivor)绘制独立三角形的GPU资源消耗

经测试,glDrawElements在iOS上无法配合变换反馈。至此,能顺利进行变换反馈、编程最简单且性能最好的绘图模式就是GL_TRIANGLES。我认为GL_TRIANGLES只是提高了顶点坐标的消费速度。不考虑能否配合变换反馈,单纯地比较性能,我觉得glDrawArrays虽然需要占用更多顶点坐标存储空间,但是,它的性能可能比glDrawElements高,类比CPU的工作方式,顶点是线性存储和读取,可以预加载接下来要绘制的顶点坐标,不会出现GPU预读取缓存失效问题。而glDrawElements使用索引只是减少顶点坐标数量,节省了占用的显存空间,由于索引指向的值可对应到存储位置分散的顶点,如此一来,每次可能需要随机读取,降低了预读取数据的命中率,影响绘制性能。因此,才推出图元重启和多重绘制(glMultiDrawElements)等接口,通过一次绘图调用实现多个分散图形的绘制来改善它们的性能。

从资源占用面板图示可知,CPU耗时比GPU高,意味着当前实现的客户端代码还有优化空间,接下来进行性能优化。

6、性能优化

在上节代码的基础上,根据性能测试信息,逐一进行代码优化。

6.1、克服气泡效应(CPU等待GPU完成)

Your application called Finish, forcing the CPU to wait for the GPU to complete some work. Performance is maximized when the CPU and GPU work in parallel. Calling Finish is generally only needed for benchmarking and certain special cases in a multi-context scenario. It may be possible to remove this Finish call to improve performance.

由于GPU处理完的数据为4字节,而Y通道的数据类型是单字节,映射GPU内存后需进行跨距内存拷贝,朴素实现代码对此加入同步操作,引起气泡效应,CPU相关性能消耗如下图所示。

iOS Video Toolbox:GPGPU加速YUV图像处理
iPad Ari 2(iOS 9.3.4)GPU绘制独立三角形的CPU性能分析

iOS Video Toolbox:GPGPU加速YUV图像处理
iPad Ari 2(iOS 9.3.4)GPU绘制独立三角形的性能分析(存在的问题及优化提示)

将绘制代码从GLKit的回调函数移到生成纹理的函数后,即,简化了主线程加锁过程及判断根据新纹理进行绘制的代码,运行新代码,发现即使主线程的CPU占用率还处于平均12%的水平,不过,计算每帧图像的CPU耗时降低至1毫秒左右,新的运行状态如下所示。

iOS Video Toolbox:GPGPU加速YUV图像处理
iPad Ari 2(iOS 9.3.4)使用顶点着色器1920×1080画三角形实现Y通道亮度减半的GPU资源消耗(CPU优化后)

对比iPhone 7的表现,可见GPU性能大幅提升。控制台输出的Metal信息以及Edit Scheme…面板上的Profile上的变化,加上2016年WWDC视频WWDC: 602 Adopting Metal Part 1侧面验证了我的猜测:iOS 10为iPhone 7只实现了Metal的驱动,OpenGL ES接口由Metal接口封装而成。

iOS Video Toolbox:GPGPU加速YUV图像处理
iPhone 7(iOS 10.0.1)使用顶点着色器1920×1080画三角形实现Y通道亮度减半的GPU资源消耗

实现上述优化的依据是,根据OpenGL ES命令执行的特点,虽然命令提交到EGL上下文后是并行执行,但总体上遵守先提交先执行(因为是命令队列)的原则,因此可以移除CPU等待GPU完成的代码,此时进行性能测试发现,唯一一次CPU等待GPU命令完成是系统的操作,我暂时没想到绕过的办法。简化glFinish等CPU同步GPU代码后,引入了一个新问题:GPU Wait on Buffer

iOS Video Toolbox:GPGPU加速YUV图像处理
iPad Ari 2(iOS 9.3.4)使用顶点着色器1920×1080画三角形实现Y通道亮度减半的GPU优化(减少CPU等GPU完成)

根据WWDC Session 416 – Tools for Tuning OpenGL ES Apps on iOS的说法,最左侧的图标为橙色表示该问题对性能影响较小。那么,我们将红色严重影响性能的问题优化成橙色问题,也是一次进步。

优化CPU端绘图调用代码后,遗留的问题是,由于纹理同时只有一个线程在使用,即是程序的主线程,故保证线程安全的代码可以完全移除。移除后,对CPU端资源消耗基本没改善,猜测是NSLock实现的是用户态锁,加解锁并不触发CPU模式(管态和物态)转变。

6.2、未初始化颜色附着

Renderbuffer #1 – Your application presented a renderbuffer, but did not render content to all pixels in the renderbuffer. The contents of any pixels not rendered by your application are unspecified. To fix this, either clear the renderbuffer before rendering the frame or add drawing calls that render to the untouched pixels.

本文档非常规使用Transform Feedback,最终并不向渲染缓冲区写入数据,而是映射回主存进行操作,故忽略此问题。

6.3、未初始化纹理数据

glEndTransformFeedback();结束变换反馈时,有如下优化提示。
Texture #1 – Your application made a drawing call using a texture that contains uninitialized data. If a draw call uses uninitialized data, the rendering results are incorrect and unpredictable. One way to fix this issue is to provide image data to any TexImage2D calls instead of a NULL pointer.

使用CVOpenGLESTextureCacheCreateTextureFromImage创建纹理时,因没显示调用glTexImage系列接口,做性能分析时总会有此提示。同样,暂且忽略。

6.4、逻辑缓冲区存储

OpenGL ES performed a logical buffer store operation. This is typically caused by switching framebuffer or not discarding buffers at the end of the rendering loop. For best performance keep logical buffer store operations to a minimum amd discard the depth buffer at the end of each frame. See the EXT_discard_framebuffer extension for more details

根据WWDC: Advances in OpenGL ES 3.0,讲座中有如下描述:

Higher penalty for frame buffer loads and stores

主讲人介绍此性能问题时说道(后面语句是我根据语音拼写的,若有错,请指出。 incur一词由我司首席科学家听一遍就写出来了,而我这种渣渣听了七八遍还是一脸茫然):

If you forget to clear the color attachments at the beginning of the frame. If you do that, we actually have to reload that, so you incur a performance penalty for that. That is logical buffer load. The way not to do that is to make sure you clear your color attachments. And logical buffer store is that if you forget to invalidate or discard your depth buffer or multiple render buffers at the end of the frame. In this case we will actually to make a copy of the attachments between frames. So you incur a performance cost for that.

从上述信息,因本程序禁用光栅化等管线后续操作,故帧缓冲区不输出数据,那么让帧缓冲区无效,减少相应的数据拷贝时间,理论上可提高性能。然而,尝试加入使帧缓冲区无效的代码反而引入新问题。

GLenum attachments[] = {GL_COLOR_ATTACHMENT0,      GL_DEPTH_ATTACHMENT,      GL_STENCIL_ATTACHMENT}; static int numberOfAttachments = sizeof(attachments) / sizeof(attachments[0]); glInvalidateFramebuffer(GL_FRAMEBUFFER,      numberOfAttachments,      attachments);

The application called glInvalidateFrameBuffer before presenting the renderbuffer. This is incorrect. If you call glInvalidateFramebuffer, do so after presenting.. renderbuffer ID = 1.

根据性能提示,glInvalidateFramebuffer需要在渲染缓冲区被显示前调用,但是EAGL的presentRenderbuffer:是GLKit在调用,不是我们主动调用,那么此操作反而引入一个新问题,暂弃之。

另外,对于glInvalidateFramebuffer,有人以前使用glDiscardFramebufferEXT,用它是不是可以实现放弃拷贝帧缓冲区数据呢?其实,glInvalidateFramebuffer是OpenGL ES 3.0版的glDiscardFramebufferEXT,WWDC有描述,真相如下图所示。

iOS Video Toolbox:GPGPU加速YUV图像处理
glDiscardFramebufferEXT与glInvalidateFramebuffer

6.5、逻辑缓冲区加载

Your application caused OpenGL ES to perform a framebuffer load operation, where the framebuffer must be loaded by the GPU before rendering. This is typically caused by failing to perform a fullscreen clear operation at the beginning of each frame. If possible, you should do so to improve performance.

同上。

6.6、冗余调用

This command was redundant:

glBindFramebuffer(GL_FRAMEBUFFER, 1u)

A GL function call that sets a piece of GL state to its current value has been detected. Minimize the number of these redundant state calls, since these calls are performing unnecessary work.

iOS Video Toolbox:GPGPU加速YUV图像处理
冗余调用的栈桢信息

使用GLKViewController时,这是框架的调用,与用户无关,因此我们修改不了,这点从栈帧信息上也可验证。

总之,如上性能问题还是因个人在OpenGL的经验较少,导致措手无策。

总结

本文档源自个人学习,主要想验证自己是否能熟练运用Transform Feedback,同时是否存在对它的适用场合的误解。

1、关于Transform Feedback的误用
Transform Feedback功能点目前从网上公开资料上看,更多运用在游戏开发场合,比如衣服抖动、鱼群流动、粒子效果等,优势是数据在顶点着色器中循环调用,无需每次进行CPU与GPU的往返拷贝。然而,我这里进行的运算并没利用它这一特性,不仅绘制的点阵数量巨大且每次计算的数据都要经过以下传输路径,进一步降低了性能:
I/O -> CPU -> GPU -> CPU ->I/O

因此,就像素级别的图像处理而言,从简化编程复杂度出发,我们应该使用光栅化,让OpenGL ES渲染管线帮我们进行每个像素点的计算。当然,为了让每个像素点都参与计算,需保证片段着色器与纹理一一对应,每个片段对应到一个像素,因为通常情况下,每个片段不一定对应到一个像素,可能在裁剪等固定管线功能后,部分像素点位于视景体之外,从而被GPU忽略,省去不必要的计算。这对于图像处理而言是不存在的,因为我们往往将数据写到源纹理一样大小的离线纹理或渲染缓冲区中。

2、关于在iPhone 6等设备上使用OpenGL ES 2.0的限制
根据WWDC: Advances in OpenGL ES 3.0,在iPhone 5s(iOS 7)等支持OpenGL ES 3.0的设备上使用OpenGL ES 2.0的上下文,则此上下文继承ES 2.0的限制。至于性能是否受影响,没给出说明文档,但主讲人肯定了2.0上下文有功能限制,使用3.0才能发挥设备的原有能力(Native Capability)。同时,A7处理器上着色器中所有浮点计算都由向量处理器实现。

All FP shader calculations performed with scalar processor

iOS Video Toolbox:GPGPU加速YUV图像处理
WWDC讲座 OpenGL ES在A7与A6处理器上的区别点

3、关于图元重启(Primitive Restart)与变换反馈配合使用的尝试
Primitive Restart Makes GPGPU Tech Sparkle指出图元重启对GPGPU运用场景是有帮助的,而我自己脑补了图元重启可以与变换反馈同时使用的内容,是我的疏忽。不过,我还是作死进行了相应的尝试,代码如下所示。

// 代码:生成索引 GLuint currentIndex = 0; for (int line = 0; line < imageSize.height; ++line) {     for (int col = 0; col < imageSize.width; ++col) {         currentPointer->x = col;         currentPointer->y = line;         ++currentPointer;         // 生成索引         indices[currentIndex] = currentIndex;         ++currentIndex;     } } // 代码:上传索引数据  GLuint elementIndices[1] = {0}; glGenBuffers(1, elementIndices); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, elementIndices[0]); glBufferData(GL_ELEMENT_ARRAY_BUFFER,      sizeof(indices),      indices,      GL_STATIC_DRAW); // 代码:开始变换 glBeginTransformFeedback(GL_TRIANGLES); glDrawElements(GL_TRIANGLES,      sizeof(indices) / sizeof(indices[0]),      GL_UNSIGNED_INT,      NULL); glEndTransformFeedback();

运行后遇到无效操作错误,如下图所示。从图中可知,索引数据已成功上传到GPU,但是,iOS提示Transform Feedback无法和glDrawElements同时运行,除非停止记录变换反馈缓冲区。然而,停止则意味着无法获取glDrawElements操作的结果数据,也就失去了变换反馈的数据源。

iOS Video Toolbox:GPGPU加速YUV图像处理
Transform Feedback与glDrawElements一起使用出现无效操作错误

但是,经查阅资料,有人在gamedev.net讨论的Transform feedback and glDrawElements求助贴说glDraw系列函数与变换反馈无关,变换反馈只是允许顶点着色器的输出被存储到一个缓冲区对象中,如何操作缓冲区对象取决于开发者。原文如下:

glDraw* is not really related to transform feedback. Transform feedback just allow the output of a vertex shader to be stored in a buffer object. What you do with that buffer object is up to you. If you configure you transform feedback varyings correctly then it should work as you explained above.

然而,我的varyings并没变化,着色器也没变,只加了索引数组和绘制模式,而Xcode则提示无法同时使用。因此,这个回复应该只是臆想,当然也可能是OpenGL与OpenGL ES限制不同。但是,Transform Feedback函数说明有如下一段被我忽略的描述:

Transform feedback is restricted to non-indexed GL_POINTS, GL_LINES, and GL_TRIANGLES.

While transform feedback is active the mode parameter to glDrawArrays must exactly match the primitiveMode specified by glBeginTransformFeedback.

因此,起初我设想是在合理的模式下,使用图元重启(Primitive Restart,配合glDrawElements)并对比图元重启对性能的影响,这在单通滤波(Single Pass)中应该是无法实现的。下面引述一段来自OpenGL.org未经验证的帖子Transform Feedback Buffer /w glDrawElements,同时此求助帖也遇到与我相同的情况,即启用glDrawElements后变换反馈无法正常工作:

Things start to break when I enable the transform feedback and try to render the output buffer with glDrawElements function. As you can guess mesh topology seems to be broken down.
Am I correct? Is there a way to use the same glDrawElements function call with the output of transform feedback (which I don’t expect naturally due to the parallel nature)?
No. That’s not what transform feedback does. It captures the triangles emitted from a draw call. Not the vertices.

Just use glDrawElements on the first pass, then glDrawArrays on the second.

由于本文场合是GPGPU,意味着每个数据点都应该参与计算,而上述的二通滤波是否能提高性能,暂时没时间验证。

4、使用glDrawArraysInstanced是否可提高性能
Transform Feedback函数说明中提及了glDrawArraysInstanced,那么此函数是否比一次性提供1920×1080共两百多万个点且只调用一次glDrawArrays快呢?验证过程详细描述在第五节【同步解码与GPU用实例化渲染(Instancing Rendering)逐三角形绘制实现YUV亮度减半】。

5、OpenGL ES在iOS 10由Metal接口封装实现
WWDC: 602 Adopting Metal Part 1证明了OpenGL ES的接口在iOS(猜测是iOS 10)上由Metal接口封装实现,相关截图如下所示。

Metal is also the foundation of these platforms.

iOS Video Toolbox:GPGPU加速YUV图像处理
OpenGL ES在iOS 10由Metal接口封装实现

6、iOS是否使用统一内存模型(Uniform Memory Model)
之前看老外的博客说iOS使用统一内存模型,CPU与GPU共享同一内存空间,在CPU和GPU之间拷贝数据速度非常快。但是,从WWDC: Advances in OpenGL ES 3.0的资料上看,CPU与GPU的内存是分开的,glMapBufferRange完成它们之间的映射操作,如下图所示。

iOS Video Toolbox:GPGPU加速YUV图像处理
WWDC CPU与GPU内存映射

7、Instancing Rendering是否比一次上传所有点阵并使用glDrawArrays快
在绘制大量相似物体时,多次调用glDraw函数会占用较多CPU时钟周期,因为glDraw系列函数是CPU请求GPU工作,有相互通信的过程,这也是许多游戏存在的CPU瓶颈。WWDC: Advances in OpenGL ES 3.0中提及到,绘制多个相似物体时,如果不使用(Instancing Rendering)实例化绘制,只用glDraw*容易挤爆OpenGL的命令缓冲区。

fill up OpenGL command buffer.

其实,实例化绘制在OpenGL ES 2.0中iOS已提供相应拓展,在ES 3.0中被收录为核心功能。

致谢

感谢同事@落雨天不撑阳伞(产自老和山职业技术学院)提供三角形网格的新思路。感谢同事@fatzhou(产自五道口男子职业技术学院)提供对绘制上述三角形网格的优化思路。虽然还没使用他们的新思路,但是在向两位请教时,自己总结出一个能成功得到数据的结论,也是有收获的。另外,在看WWDC: Advances in OpenGL ES 3.0视频时,incur一词由另一位同事在毫无上下文帮助的情况下一次就听出来。至此,在下已将膝盖献给了三位同事。
感谢@平平淡淡才是真大大回顾了我的渣渣实现代码。
感谢@hhhh大大给的Flash上实现图像处理的资料,最近加班,来不及看。

参考与推荐阅读

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » iOS Video Toolbox:GPGPU加速YUV图像处理

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址