实时渲染不是梦:通过共享内存优化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 外接纹理的原理
先回顾下前置知识,看看官方提供的外接纹理机制究竟是怎样运行的。
图中红色块,是我们自己要编写的native代码,黄色是Flutter engine的内部代码逻辑。整体流程分为注册纹理,和整体的纹理渲染逻辑。
注册纹理
创建一个对象,实现
FlutterTexture
协议,该对象用来管理具体的纹理数据通过
FlutterTextureRegistry
来注册第一步的FlutterTexture
对象,获取一个Flutter纹理id将该id通过channel机制传递给dart侧,dart侧就能够通过
Texture
这个widget来使用纹理了,参数就是id
纹理渲染
dart侧声明一个
Texture
widget,表明该widget实际渲染的是native提供的纹理engine侧拿到layerTree,layerTree的
TextureLayer
节点负责外接纹理的渲染首先通过dart侧传递的id,找到先注册的
FlutterTexture
,该flutterTexture是我们自己用native代码实现的,其核心是实现了copyPixelBuffer
方法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 theCVImageBufferRef
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 orGL_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 callingglFlush()
.
从文档里面,我们了解到几个关键点:
返回的纹理对象,是直接映射到了CVPixelBufferRef对象的内存的
这块buffer内存,其实是可以同时被CPU和GPU访问的,我们只需要遵循如下的规则:
GPU访问的时候,该
CVPixelBuffer
,不能够处于lock状态。
使用过pixelbuffer的同学应该都知道,通常CPU操作pixelbuffer对象的时候,要先进行lock操作,操作完毕再unlock。所以这里也容易理解,GPU使用纹理的时候,其必然不能够同时被CPU操作。
CPU访问的时候,要保证GPU已经渲染完成,通常是指在
glFlush()
调用之后。
这里也容易理解,CPU要读写这个buffer的时候,要保证关联的纹理不能正在被OpenGL渲染。
我们用instrument的allocation来验证一下:
instrument的结果,也能够印证文档中的结论。只有在创建pixelBuffer的时候,才分配了内存,而映射到纹理的时候,并没有新的内存分配。
这里也能印证我们的结论,创建pixelBuffer的时候,才分配了内存,映射到纹理的时候,并没有新的内存分配。
共享内存方案
既然了解到CVPixelBuffer对象,实际上是可以桥接一个OpenGL的纹理的,那我们的整体解决方案就水到渠成了,可以看看下面这个图
关键点在于,首先需要创建pixelBuffer对象,并分配内存。然后在native gl环境和flutter gl环境里面分别映射一个纹理对象。这样,在2个独立的gl环境里面,我们都有各自的纹理对象,但实际上其内存都被映射到同一个CVPixelBuffer
上。在实际的每一帧渲染流程里面,native环境做渲染到纹理,而flutter环境里面则是从纹理读取数据。
Demo演示
这里我写了个小demo来验证下实际效果,demo的主要逻辑是以60FPS的帧率,渲染一个旋转的三角形到一个pixelBuffer映射的纹理上。然后每帧绘制完成之后,通知flutter侧来读取这个pixelBuffer对象去做渲染。
核心代码展示如下:
- (void)createCVBufferWith:(CVPixelBufferRef *)target withOutTexture:(CVOpenGLESTextureRef *)texture {
// 创建纹理缓存池,这个不是重点
CVReturn err = CVOpenGLESTextureCacheCreate(kCFAllocatorDefault, NULL, _context, NULL, &_textureCache);
// 其他代码略
// 核心参数是这个,共享内存必须要设置这个kCVPixelBufferIOSurfacePropertiesKey
CFDictionarySetValue(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/