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

Zero-copy Camera Processing and Rendering Pipeline on Android

我需要对实时相机数据(仅来自 Y 平面)执行 CPU 端只读处理,然后在 GPU 上进行渲染。在处理完成之前不应渲染帧(因此我并不总是想渲染来自相机的最新帧,只是 CPU 端已完成处理的最新帧)。渲染与相机处理脱钩,目标是 60 FPS,即使相机帧到达的速率低于此值。

有一个相关但更高级别的问题:

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

感谢@fadden 对上面链接问题的回答,我现在了解 android camera2 API 的 "parallel output" 功能在各个输出队列之间共享缓冲区,所以应该不涉及数据的任何副本,至少在现代 android.

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

想到的一个解决方案是拥有多个 SurfaceTextures - 我们的每个应用程序端缓冲区中都有一个(我们目前使用 3 个)。使用该方案,当我们获得新的相机帧时,我们将从应用程序端池中获得一个空闲缓冲区。然后我们将在 ImageReader 上调用 acquireLatestImage() 以获取要处理的数据,并在空闲缓冲区中的 SurfaceTexture 上调用 updateTexImage()。在渲染时,我们只需要确保来自 "in display" 缓冲区的 SufaceTexture 是绑定到 GL 的那个,并且大多数时候一切都应该同步(正如@fadden 评论的那样,调用 updateTexImage()acquireLatestImage() 但那个时间 window 应该足够小以使其罕见,并且无论如何使用缓冲区中的时间戳可能是可检测和可修复的。

我在文档中注意到 updateTexImage() 只能在 SurfaceTexture 绑定到 GL 上下文时调用,这表明我也需要在相机处理线程中使用 GL 上下文,以便相机线程可以在 "free" 缓冲区中的 SurfaceTexture 上执行 updateTexImage(),同时渲染线程仍然能够从 "in display" 缓冲区中的 SurfaceTexture 进行渲染。

所以,对于问题:

  1. 这看起来是一个明智的方法吗?
  2. SurfaceTextures 基本上是共享缓冲池的轻型包装器,还是它们消耗一些有限的硬件资源并且应该谨慎使用?
  3. SurfaceTexture 调用是否足够便宜,以至于使用多个调用仍然比仅复制数据大获全胜?
  4. 计划让两个线程具有不同的 GL 上下文并在每个线程中绑定不同的 SurfaceTexture 是否可行,或者我是否要求一个充满痛苦和错误的驱动程序的世界?

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

有趣的问题。

背景素材

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

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

SurfaceTexture,a/k/a GLConsumer,不进行大量处理。当帧从源(在您的情况下为相机)到达时,它使用一些 EGL 函数将缓冲区 "wrap" 作为 "external" 纹理。如果没有要在其中工作的 EGL 上下文,您将无法执行这些 EGL 操作,这就是为什么必须将 SurfaceTexture 附加到其中的原因,以及为什么如果当前上下文错误则不能将新帧放入纹理中。您可以从 the implementation of updateTexImage() 中看到,它正在用缓冲区队列、纹理和栅栏做很多神秘的事情,但是 none 其中需要复制像素数据。您真正占用的唯一系统资源是 RAM,如果您要捕获高分辨率图像,这并非微不足道。

连接数

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

可以创建在它们之间共享某些内容(包括纹理)的 EGL 上下文,但是如果这些上下文位于不同的线程上,则在更新纹理时必须非常小心。 Grafika 提供了 getting it wrong.

的一个很好的例子

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

实施

如果您经常连接和断开 BufferQueues,将会产生大量开销。所以如果你想要三个 SurfaceTextures 接收缓冲区,你需要将所有三个连接到 Camera2 的输出,并让它们都接收 "buffer broadcast"。然后你 updateTexImage() 以循环方式。由于 SurfaceTexture 的 BufferQueue 在 "async" 模式下运行,您应该始终在每次调用时获得最新的帧,而不需要 "drain" 队列。

直到 Lollipop 时代的 BufferQueue 多输出变化和 Camera2 的引入,这种安排才真正成为可能,所以我不知道以前是否有人尝试过这种方法。

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

A new OpenGL ES texture object is created and populated with the SurfaceTexture image frame that was current at the time of the last call to detachFromGLContext().

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

可惜 ImageReader 缺少 getTimestamp() 调用,因为这会大大简化从相机匹配缓冲区的过程。

结论

使用多个 SurfaceTextures 来缓冲输出是可能的,但很棘手。我可以看到乒乓缓冲区方法的潜在优势,其中一个 ST 用于在 thread/context A 中接收帧,而另一个 ST 用于在 thread/context B 中进行渲染,但是既然你重新实时运行我认为额外的缓冲没有价值,除非你试图填补时间。

一如既往,Android System-Level Graphics Architecture doc 推荐阅读。