在Android上零拷贝相机处理和渲染stream水线

我需要在实时摄像机数据(仅来自Y平面)上执行CPU端只读过程,然后在GPU上进行渲染。 在处理完成之前不应该渲染帧(所以我不总是想渲染摄像机的最新帧,只是CPU端已经完成处理的最新帧)。 渲染与摄像机处理是分离的,即使摄像机帧的速率低于60帧,其目标也是60帧/秒。

有一个相关的,但更高层次的问题在: 最低的开销摄像头CPU到GPU的方法在Android上

要更详细地描述当前的设置:我们有一个应用程序端的缓冲池用于摄像机数据,其中缓冲区是“空闲”,“正在显示”或“正在显示”。 当来自摄像机的新帧到达时,我们抓取一个空闲的缓冲区,在那里存储帧(或者如果实际数据在系统提供的缓冲池中的引用),执行处理并将结果存入缓冲区,然后设置缓冲区“等待显示”。 在渲染器线程中,如果在渲染循环的开始处有任何缓冲区“暂挂显示”,我们将其locking为“正在显示”中的一个,渲染相机,并使用从相同的计算得到的已处理信息渲染其他内容相机框架。

感谢@ fadden对上面提到的问题的回应我现在明白android camera2 API的“并行输出”function可以在各种输出队列之间共享缓冲区,所以不应该涉及数据上的任何副本,至less在现代的android上。

在评论中有一个build议,我可以同时lockingSurfaceTexture和ImageReader输出,只是“坐在缓冲区”,直到处理完成。 不幸的是,我不认为这适用于我的情况,因为我们仍然希望以60 FPS的速度进行分离渲染,并且仍然需要访问前一帧,而新的一个正在处理,以确保事情不会得到不同步。

想到的一个解决scheme是拥有多个SurfaceTextures – 每个应用程序端缓冲区都有一个(我们目前使用3个)。 有了这个scheme,当我们得到一个新的相机帧,我们将从我们的应用程序池获得一个免费的缓冲区。 然后我们调用ImageReader的acquireLatestImage()来获取数据进行处理,并在空闲缓冲区的SurfaceTexture上调用updateTexImage() 。 在呈现的时候,我们只需要确保“显示”缓冲区中的SufaceTexture是绑定到GL的那个,并且大部分时间应该是同步的(@fadden评论说在调用updateTexImage()acquireLatestImage()但那个时间窗口应该足够小,使得它很less见,也许使用缓冲区中的时间戳是可以解决的。

我在文档中注意到updateTexImage()只能在SurfaceTexture绑定到GL上下文时调用,这意味着我需要在相机处理线程中使用GL上下文,因此相机线程可以在SurfaceTexture上执行updateTexImage()在“空闲”缓冲区中,而渲染线程仍然能够从“显示”缓冲区中的SurfaceTexture渲染。

所以,对于这个问题:

  1. 这似乎是一个明智的做法?
  2. SurfaceTextures基本上是共享缓冲池的简单包装,还是消耗一些有限的硬件资源,应该谨慎使用?
  3. SurfaceTexture的调用是否足够便宜,以至于复制数据仍然是一个巨大的胜利?
  4. 是计划有两个不同的GL上下文线程与不同的SurfaceTexture绑定在每个可能的工作,或者我要求一个痛苦和越野车司机的世界?

这听起来很有希望,我会去给它一个去; 但是如果有人(基本上@fadden!)知道我忽视了哪些内部细节,这会让这个主意变得糟糕,那么在这里值得问一下。

有趣的问题。

背景的东西

具有独立上下文的多个线程是非常普遍的。 每个使用硬件加速视图渲染的应用程序在主线程上都有一个GLES上下文,所以任何使用GLSurfaceView的应用程序(或者使用SurfaceView或TextureView和一个独立的渲染线程来滚动自己的EGL)都会使用多个上下文。

每个TextureView都有一个SurfaceTexture,所以任何使用多个TextureView的应用程序在单个线程上都有多个SurfaceTextures。 (该框架实际上有一个错误 ,导致多个TextureViews的问题,但这是一个高层次的问题,而不是驱动程序的问题。)

SurfaceTexture,a / k / a GLConsumer,并没有做很多的处理。 当一帧来自源(在你的情况下,相机),它使用一些EGLfunction“包装”缓冲区作为“外部”纹理。 如果没有EGL上下文,不能执行这些EGL操作,这就是为什么SurfaceTexture必须连接到一个,以及为什么如果错误的上下文是最新的,你不能把新的框架放到纹理中。 你可以从updateTexImage()的实现中看到,它使用缓冲队列,纹理和栅栏来做很多神秘的事情,但是它们都不需要复制像素数据。 你唯一的系统资源是RAM,如果你捕捉高分辨率的图像,这是不可置信的。

连接

EGL上下文可以在线程之间移动,但一次只能在一个线程上“当前”。 从多个线程同时访问将需要很多不希望的同步。 给定的线程只有一个“当前”上下文。 OpenGL API从具有全局状态的单线程演变为multithreading,而不是重写API,它们只是将状态推入到线程本地存储中,因此也就是“current”的概念。

可以创build在它们之间共享某些东西的EGL上下文(包括纹理),但是如果这些上下文位于不同的线程上,则在更新纹理时必须非常小心。 Grafika提供了一个错误的例子。

SurfaceTexturesbuild立在BufferQueues之上,它有一个生产者 – 消费者结构。 SurfaceTextures有趣的地方在于它们包含两个方面,所以你可以在一个进程中提供数据,并在一个进程内把数据拉出另一个进程(不像SurfaceView,而消费者很远)。 像所有Surface的东西一样,它们build立在Binder IPC之上,所以你可以从一个线程提供Surface,并且在不同的线程(或进程)中安全地updateTexImage() )。 API的安排是为了在客户端(你的进程)上创buildSurfaceTexture,然后把引用传给生产者(例如主要在mediaserver进程中运行的摄像头)。

履行

如果你经常连接和断开BufferQueue,你会引发一堆开销。 所以如果你想有三个SurfaceTextures接收缓冲区,你需要连接所有三个Camera2的输出,并让他们都收到“缓冲广播”。 然后你以循环的方式updateTexImage() 。 由于SurfaceTexture的BufferQueue以“asynchronous”模式运行,因此每次调用时都应该获取最新的帧,而不需要“排空”队列。

直到棒棒堂时代的BufferQueue多输出改变和Camera2的引入,这种安排才真正有可能,所以我不知道有没有人曾经尝试过这种方法。

所有的SurfaceTextures都将被附加到相同的EGL上下文中,理想的情况是在View UI线程以外的线程中,所以你不必争取当前的东西。 如果您想从另一个线程中的第二个上下文访问纹理,则需要使用SurfaceTexture 附加/分离 API调用,它明确支持这种方法:

创build一个新的OpenGL ES纹理对象,并使用上次调用detachFromGLContext()时最新的SurfaceTexture图像框填充该对象。

请记住,切换EGL上下文是消费者端操作,与摄像机的连接没有关系,这是生产者端操作。 在上下文之间移动SurfaceTexture所涉及的开销应该小于updateTexImage() – 但是您需要采取通常的步骤来确保线程之间进行通信时的同步。

这太糟糕了ImageReader没有getTimestamp()调用,因为这将大大简化从摄像头匹配缓冲区。

结论

使用多个SurfaceTextures缓冲输出是可能的,但棘手。 我可以看到乒乓缓冲方法的潜在优势,其中一个ST用于接收线程/上下文A中的帧,而另一个ST用于线程/上下文B中的渲染,但由于您正在以真实我不认为有额外的缓冲价值,除非你正在尝试填补时间。

与往常一样,推荐阅读Android系统级graphics体系结构文档 。