10

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

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

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

感谢@fadden 对上面链接的问题的回复,我现在了解了 android camera2 API 的“并行输出”功能在各种输出队列之间共享缓冲区,因此至少在现代 android 上不应该涉及数据的任何副本。

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

想到的一种解决方案是拥有多个 SurfaceTexture - 我们的每个应用程序端缓冲区中都有一个(我们目前使用 3 个)。使用该方案,当我们获得一个新的相机帧时,我们将从我们的应用端池中获得一个空闲缓冲区。然后我们会调用acquireLatestImage()ImageReader 来获取数据进行处理,并调用updateTexImage()空闲缓冲区中的 SurfaceTexture。在渲染时,我们只需要确保“显示中”缓冲区中的 SufaceTexture 是绑定到 GL 的那个,并且大部分时间一切都应该同步(正如@fadden 评论的那样,调用updateTexImage()and之间存在竞争,acquireLatestImage()但是时间窗口应该足够小以使其很少见,并且无论如何使用缓冲区中的时间戳可能是可检测和可修复的)。

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

所以,对于问题:

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

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

4

1 回答 1

10

有趣的问题。

背景资料

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

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

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

连接

EGL 上下文可以在线程之间移动,但一次只能在一个线程上“当前”。来自多个线程的同时访问将需要大量不需要的同步。给定线程只有一个“当前”上下文。OpenGL API 从具有全局状态的单线程演变为多线程,而不是重写 API,他们只是将状态推送到线程本地存储中……因此有了“当前”的概念。

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

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

执行

如果您不断地连接和断开 BufferQueues,将会产生大量开销。所以如果你想让三个 SurfaceTexture 接收缓冲区,你需要将所有三个连接到 Camera2 的输出,并让它们都接收“缓冲区广播”。然后你updateTexImage()以循环方式进行。由于 SurfaceTexture 的 BufferQueue 以“异步”模式运行,因此您应该始终在每次调用时获取最新帧,而无需“排空”队列。

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

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

一个新的 OpenGL ES 纹理对象被创建并填充了在最后一次调用 detachFromGLContext() 时当前的 SurfaceTexture 图像帧。

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

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

结论

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

与往常一样,推荐阅读Android System-Level Graphics Architecture 文档。

于 2016-06-02T20:59:36.553 回答