Android上的零拷贝相机处理和渲染管道

Zero-copy Camera Processing and Rendering Pipeline on Android

本文关键字:管道 处理 相机 拷贝 Android      更新时间:2023-10-16

我需要对实时相机数据(仅来自Y平面)进行CPU端只读处理,然后在GPU上渲染。在处理完成之前不应该渲染帧(所以我不总是想渲染相机的最新帧,只想渲染CPU端完成处理的最新一帧)。渲染与相机处理解耦,即使相机帧的到达速率低于60 FPS,渲染也以60 FPS为目标。

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

为了更详细地描述当前的设置:我们有一个用于相机数据的应用程序端缓冲池,其中的缓冲区要么是"空闲"、"显示中"或"待显示"。当来自相机的新帧到达时,我们获取一个空闲的缓冲区,将帧存储在那里(如果实际数据在某个系统提供的缓冲池中,则存储对帧的引用),进行处理并将结果存储在缓冲区中,然后将缓冲区设置为"挂起显示"。在渲染器线程中,如果在渲染循环开始时有任何缓冲区"挂起显示",我们将其锁定为"显示中"的缓冲区,渲染相机,并使用从同一相机帧计算的处理信息渲染其他内容。

由于@fadden对上面链接的问题的回答,我现在理解了android camera2 API的"并行输出"功能在不同的输出队列之间共享缓冲区,因此不应该涉及数据的任何副本,至少在现代android上是这样。

在一条评论中,有人建议我可以同时锁存SurfaceTexture和ImageReader输出,然后"坐在缓冲区上",直到处理完成。不幸的是,我认为这不适用于我的情况,因为我们仍然希望以60 FPS的速度驱动解耦渲染,并且在处理新帧时仍然需要访问前一帧,以确保事情不会不同步。

脑海中浮现的一个解决方案是拥有多个SurfaceTextures——每个应用程序端缓冲区中都有一个(我们目前使用3个)。使用该方案,当我们获得一个新的相机框架时,我们将从应用程序侧池中获得一个免费缓冲区。然后,我们在ImageReader上调用acquireLatestImage()以获取要处理的数据,并在空闲缓冲区中的SurfaceTexture上调用updateTexImage()。在渲染时,我们只需要确保"显示中"缓冲区中的SufaceTexture是绑定到GL的,并且所有东西在大多数时间都应该同步(正如@fadden所评论的,在调用updateTexImage()acquireLatestImage()之间存在竞争,但该时间窗口应该足够小,以使其变得罕见,并且可能是可伸缩的,并且无论如何都可以使用缓冲区的时间戳来修复)。

我在文档中注意到,只有当SurfaceTexture绑定到GL上下文时,才能调用updateTexImage(),这表明我在相机处理线程中也需要GL上下文,这样相机线程就可以在"空闲"缓冲区中的SurfaceTexture上执行updateTexImage(),而渲染线程仍然可以从"显示中"缓冲区的Surface纹理进行渲染。

因此,对于问题:

  1. 这似乎是一个明智的方法吗
  2. SurfaceTextures基本上是围绕共享缓冲池的轻量级包装器,还是它们消耗了一些有限的硬件资源,应该谨慎使用
  3. SurfaceTexture调用是否都足够便宜,以至于使用多个调用仍将是复制数据的一大优势
  4. 计划让两个具有不同GL上下文的线程,每个线程绑定不同的SurfaceTexture,这可能奏效吗?还是我要求一个充满痛苦和bug驱动程序的世界

这听起来很有希望,我要试一试;但我认为这里值得一问,以防有人(基本上是@fadden!)知道我忽略的任何内部细节,这会让这个主意变得糟糕。

有趣的问题。

背景资料

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

每个TextureView内部都有一个SurfaceTexture,因此任何使用多个TextureView的应用程序在一个线程上都有多个SurfaceTextures。(该框架在实现中实际上有一个错误,导致多个TextureView出现问题,但这是一个高级问题,而不是驱动程序问题。)

SurfaceTexture,一个/k/a的GLConsumer,并没有做很多处理。当帧从源(在您的情况下,是相机)到达时,它会使用一些EGL函数将缓冲区"包裹"为"外部"纹理。如果没有EGL上下文,您就无法执行这些EGL操作,这就是为什么SurfaceTexture必须附加到一个上下文的原因,也是为什么如果错误的上下文是当前上下文,则无法将新帧放入纹理的原因。从updateTexImage()的实现中可以看出,它在缓冲队列、纹理和围栏方面做了很多神秘的事情,但都不需要复制像素数据。你真正占用的唯一系统资源是RAM,如果你要拍摄高分辨率的图像,这一点也不小。

连接

EGL上下文可以在线程之间移动,但一次只能是一个线程上的"当前"上下文。来自多个线程的同时访问将需要大量不需要的同步。给定线程只有一个"当前"上下文。OpenGL API从具有全局状态的单线程发展到多线程,他们没有重写API,而是将状态推入线程存储。。。因此产生了"电流"的概念。

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

SurfaceTextures构建在BufferQueues之上,后者具有生产者-消费者结构。SurfaceTextures的有趣之处在于,它们包括两侧,因此您可以在一个过程中将数据输入一侧,然后从另一侧提取(不像SurfaceView,消费者离得很远)。像所有Surface材料一样,它们都是在Binder IPC的顶部构建的,因此您可以从一个线程中提供Surface,并在不同的线程(或进程)中安全地提供updateTexImage()。API的安排是,您可以在消费者端(您的进程)创建SurfaceTexture,然后将引用传递给生产者(例如,主要在mediaserver进程中运行的相机)。

实施

如果您不断地连接和断开BufferQueues,则会导致大量开销。因此,如果你想让三个SurfaceTextures接收缓冲区,你需要将这三个都连接到Camera2的输出,并让它们都接收"缓冲区广播"。然后你以循环方式updateTexImage()。由于SurfaceTexture的BufferQueue以"异步"模式运行,因此每次调用都应该获得最新的帧,而无需"耗尽"队列。

直到棒棒糖时代的BufferQueue多输出改变和Camera2的引入,这种安排才真正实现,所以我不知道以前是否有人尝试过这种方法。

所有的SurfaceTextures都将附加到同一个EGL上下文,理想情况下是在View UI线程之外的线程中,因此您不必为当前的内容而争论。如果您想从另一个线程中的第二个上下文访问纹理,则需要使用SurfaceTexture attach/detach API调用,该调用明确支持这种方法:

将创建一个新的OpenGL ES纹理对象,并使用上次调用detachFromGLContext()时的当前SurfaceTexture图像帧填充该对象。

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

糟糕的是ImageReader缺少getTimestamp()调用,因为这将大大简化从相机匹配缓冲区的过程。

结论

使用多个SurfaceTextures来缓冲输出是可能的,但很棘手。我可以看到乒乓缓冲区方法的潜在优势,其中一个ST用于在线程/上下文a中接收帧,而另一个ST则用于在线程或上下文B中渲染,但由于您是实时操作的,我认为额外的缓冲没有价值,除非您试图补充时间。

一如既往,建议阅读Android系统级图形架构文档。