前言

咳咳,看了咸鱼这篇《万万没想到——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的内部代码逻辑。整体流程分为注册纹理,和整体的纹理渲染逻辑。

注册纹理

  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来创建纹理的方法,就能够得到答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void IOSExternalTextureGL::CreateTextureFromPixelBuffer() {
// 注意buffer_ref就是copyPixelBuffer返回的buffer数据
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来验证一下:

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

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

共享内存方案

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

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

Demo演示

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

核心代码展示如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
- (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共享内存的场景的。

完整的demo代码在这里flutter_texture