vlambda博客
学习文章列表

实时渲染不是梦:通过共享内存优化Flutter外接纹理的渲染性能

前言


之前看了一篇闲鱼的文章《万万没想到——Flutter这样外接纹理》,了解到Flutter提供一种机制,可以将native的纹理共享给Flutter来进行渲染。但是,由于Flutter获取native纹理的数据类型是CVPixelBuffer,导致native纹理需要经过GPU->CPU->GPU的转换过程消耗额外性能,这对于需要实时渲染的音视频类需求,是不可接受的。

闲鱼这边的解决方案是修改了Flutter engine的代码,将Flutter的gl环境和native的gl环境通过ShareGroup来联通,避免2个环境的纹理传递还要去cpu内存绕一圈。此方案能够解决内存拷贝的性能问题,但暴露Flutter的gl环境,毕竟是一个存在风险的操作,给以后的Flutter渲染问题定位也增加了复杂度。

所以,有没有一个完美、简便的方案呢?

答案就是利用CVPixelBuffer的共享内存机制。

Flutter 外接纹理的原理


先回顾下前置知识,看看官方提供的外接纹理机制究竟是怎样运行的。


实时渲染不是梦:通过共享内存优化Flutter外接纹理的渲染性能


图中红色块,是我们自己要编写的native代码,黄色是Flutter engine的内部代码逻辑。整体流程分为注册纹理,和整体的纹理渲染逻辑。


注册纹理


  1. 创建一个对象,实现FlutterTexture协议,该对象用来管理具体的纹理数据

  2. 通过FlutterTextureRegistry来注册第一步的FlutterTexture对象,获取一个Flutter纹理id

  3. 将该id通过channel机制传递给dart侧,dart侧就能够通过Texture这个widget来使用纹理了,参数就是id


纹理渲染


  1. dart侧声明一个Texture widget,表明该widget实际渲染的是native提供的纹理

  2. engine侧拿到layerTree,layerTree的TextureLayer节点负责外接纹理的渲染

  3. 首先通过dart侧传递的id,找到先注册的FlutterTexture,该flutterTexture是我们自己用native代码实现的,其核心是实现了copyPixelBuffer方法

  4. flutter engine调用copyPixelBuffer拿到具体的纹理数据,然后交由底层进行GPU渲染


CVPixelBuffer格式分析


一切问题的根源就在这里了:CVPixelBuffer。从上面flutter外接纹理的渲染流程来看,native纹理到flutter纹理的数据交互,是通过copyPixelBuffer传递的,其参数就是CVPixelBuffer而前面咸鱼文章里面说的性能问题,就来自于纹理与CVPixelBuffer之间的转换。

那么,如果CVPixelBuffer能够和OpenGL的纹理同享同一份内存拷贝,GPU -> CPU -> GPU的性能瓶颈,是否就能够迎刃而解了呢?其实我们看一下flutter engine里面利用CVPixelBuffer来创建纹理的方法,就能够得到答案:

void IOSExternalTextureGL::CreateTextureFromPixelBuffer() {
CVOpenGLESTextureRef texture; CVReturn err = CVOpenGLESTextureCacheCreateTextureFromImage( kCFAllocatorDefault, cache_ref_, buffer_ref_, nullptr, GL_TEXTURE_2D, GL_RGBA,static_cast<int>(CVPixelBufferGetWidth(buffer_ref_)),static_cast<int>(CVPixelBufferGetHeight(buffer_ref_)), GL_BGRA, GL_UNSIGNED_BYTE, 0, &texture);if (err != noErr) { FML_LOG(WARNING) << "Could not create texture from pixel buffer: " << err; } else { texture_ref_.Reset(texture); }}

flutter engine是使用CVOpenGLESTextureCacheCreateTextureFromImage这个接口来从CVPixelBuffer对象创建OpenGL纹理的,那么这个接口实际上做了什么呢?我们来看一下官方文档


This function either creates a new or returns a cached CVOpenGLESTextureRef texture object mapped to the CVImageBufferRef and associated parameters. This operation creates a live binding between the image buffer and the underlying texture object. The EAGLContext associated with the cache may be modified to create, delete, or bind textures. When used as a source texture or GL_COLOR_ATTACHMENT, the image buffer must be unlocked before rendering. The source or render buffer texture should not be re-used until the rendering has completed. This can be guaranteed by calling glFlush().


从文档里面,我们了解到几个关键点:


  1. 返回的纹理对象,是直接映射到了CVPixelBufferRef对象的内存的

  2. 这块buffer内存,其实是可以同时被CPU和GPU访问的,我们只需要遵循如下的规则:


    • GPU访问的时候,该CVPixelBuffer,不能够处于lock状态。


      使用过pixelbuffer的同学应该都知道,通常CPU操作pixelbuffer对象的时候,要先进行lock操作,操作完毕再unlock。

      所以这里也容易理解,GPU使用纹理的时候,其必然不能够同时被CPU操作。

    • CPU访问的时候,要保证GPU已经渲染完成,通常是指在glFlush()调用之后。


      这里也容易理解,CPU要读写这个buffer的时候,要保证关联的纹理不能正在被OpenGL渲染。


我们用instrument的allocation来验证一下:

实时渲染不是梦:通过共享内存优化Flutter外接纹理的渲染性能

instrument的结果,也能够印证文档中的结论。只有在创建pixelBuffer的时候,才分配了内存,而映射到纹理的时候,并没有新的内存分配。

这里也能印证我们的结论,创建pixelBuffer的时候,才分配了内存,映射到纹理的时候,并没有新的内存分配。

共享内存方案


既然了解到CVPixelBuffer对象,实际上是可以桥接一个OpenGL的纹理的,那我们的整体解决方案就水到渠成了,可以看看下面这个图

实时渲染不是梦:通过共享内存优化Flutter外接纹理的渲染性能


关键点在于,首先需要创建pixelBuffer对象,并分配内存。然后在native gl环境和flutter gl环境里面分别映射一个纹理对象。这样,在2个独立的gl环境里面,我们都有各自的纹理对象,但实际上其内存都被映射到同一个CVPixelBuffer上。在实际的每一帧渲染流程里面,native环境做渲染到纹理,而flutter环境里面则是从纹理读取数据。


Demo演示

这里我写了个小demo来验证下实际效果,demo的主要逻辑是以60FPS的帧率,渲染一个旋转的三角形到一个pixelBuffer映射的纹理上。然后每帧绘制完成之后,通知flutter侧来读取这个pixelBuffer对象去做渲染。


实时渲染不是梦:通过共享内存优化Flutter外接纹理的渲染性能


核心代码展示如下:

- (void)createCVBufferWith:(CVPixelBufferRef *)target withOutTexture:(CVOpenGLESTextureRef *)texture {
// 创建纹理缓存池,这个不是重点 CVReturn err = CVOpenGLESTextureCacheCreate(kCFAllocatorDefault, NULL, _context, NULL, &_textureCache);
// 其他代码略// 核心参数是这个,共享内存必须要设置这个kCVPixelBufferIOSurfacePropertiesKeyCFDictionarySetValue(attrs, kCVPixelBufferIOSurfacePropertiesKey, empty);
// 分配pixelBuffer对象的内存,注意flutter需要的是BGRA格式 CVPixelBufferCreate(kCFAllocatorDefault, _size.width, _size.height, kCVPixelFormatType_32BGRA, attrs, target);
// 映射上面的pixelBuffer对象到一个纹理上 CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, _textureCache, *target, NULL, GL_TEXTURE_2D, GL_RGBA, _size.width, _size.height, GL_BGRA, GL_UNSIGNED_BYTE, 0, texture);
CFRelease(empty);CFRelease(attrs);}
- (CVPixelBufferRef)copyPixelBuffer {// 实现FlutterTexture协议的接口,每次flutter是直接读取我们映射了纹理的pixelBuffer对象 CVBufferRetain(_target);return _target;}
- (void)initGL { _context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]; [EAGLContext setCurrentContext:_context];
// 先调用上面的函数创建共享内存的pixelBuffer和texture对象 [self createCVBufferWith:&_target withOutTexture:&_texture];
// 创建帧缓冲区 glGenFramebuffers(1, &_frameBuffer); glBindFramebuffer(GL_FRAMEBUFFER, _frameBuffer);
// 将纹理附加到帧缓冲区上 glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, CVOpenGLESTextureGetName(_texture), 0);
// 略}

关键代码都添加了注释,这里就不分析了


我们从上面的gif图上可以看到整个渲染过程是十分流畅的,最后看displayLink的帧率也能够达到60FPS。该demo是可以套用到其他的需要CPU与GPU共享内存的场景的。

相关链接

《万万没想到——flutter这样外接纹理》

https://juejin.im/post/5b7b9051e51d45388b6aeceb


完整demo代码

https://github.com/luoyibu/flutter_texture


原文链接

http://www.luoyibu.cn/posts/9703/



活动推荐






关注云加社区,回复 加群 加读者群



这个方案赞么?