5 循环帧缓冲区(Cyclic Frame Buffer)
5.1 引言
在某些应用中,特别是视频图像的实时处理中,通常希望维护一组固定大小的缓冲区,每个缓冲区包含一帧。这允许根据需 要跳过帧或重复播放帧。
在帧采集(frame grabber)应用中,可以通过跳过一个或多个帧直至有空闲缓冲区来处理溢出(overflow)情况。例如,在 实时视图应用中,当观看窗口移动或调整大小时,可能发生这种溢出。像这样丢弃帧可以防止视频源的连续数据流中断,同时保持低延 迟。
在帧回放(frame replay)应用中(例如显示实时输出的屏幕),当没有新帧可显示时,会重复输出当前图像。这解决了源 (例如磁盘)暂时停滞导致显示图像短暂冻结的情况。虽然不够优雅,但总比流失去同步要好。在许多情况下,这种图像重复机制虽然有些 粗糙,但在克服帧率差异方面效果良好,特别是当输出帧率远高于输入帧率时(例如从 30 fps 到 60 fps)。
本节讨论如何修改在第 4.4 节中介绍的 FIFO 演示应用程序,以管理此类缓冲区集合。
5.2 适配 FIFO 示例代码
维护循环帧缓冲区集合与维护 FIFO 之间存在相似性。实际上,如果 FIFO 中的每个字节代表一个帧缓冲区,那么 FIFO 中某 个字节是否可读或可写,就相当于整个帧缓冲区是否可读或可写。
例如,假设一个帧采集应用分配了四个帧缓冲区来存储接收到的图像数据。再假设设置了一个四字节的 FIFO 来帮助管理这 四个帧缓冲区,方式如下:
接收数据的线程从第一个帧缓冲区开始,以循环方式继续到下一个。在开始写入新帧缓冲区之前,该线程检查四字节 FIFO 是否未满(full)。完成一个帧缓冲区后,它向 FIFO 写入一个字节,如果 FIFO 未满则转到下一个。
消费图像数据的线程以相同顺序循环遍历帧缓冲区。在尝试读取新帧缓冲区之前,它检查四字节 FIFO 是否非空 (empty)。当完成一个帧缓冲区并准备好转到下一个时,它从 FIFO 读取一个字节。
通过坚持此约定,可以保证接收数据的线程永远不会覆盖尚未被消费的帧缓冲区,并且消费线程永远不会尝试读取包含无效 数据的帧缓冲区。实际上,FIFO 中的字节数代表集合中有效帧缓冲区的数量。
请注意,写入和读取的字节值无关紧要,因此实际上无需分配这四个字节的内存并在其中存储数据。只有 FIFO 的握手机制 起作用。
因此,可以按原样采用第 4.6 节中概述的 FIFO API:
-
使用 size 参数(帧缓冲区的数量)调用 fifo_init() 函数(回顾一下,size 可以为任意整 数)。fifo_init() 将分配并锁定 FIFO 的内存,该内存永远不会被使用(因为每个字节仅象征一个帧缓冲区)。这种内存浪费可以忽略不计,但 可以删除代码中的相关部分以避免将来混淆。
-
调用 fifo_request_drain() 函数以获取要读取的帧缓冲区。info->position 将包含要使用的帧缓冲区的索引(编号从 0 开始)。如果 没有帧缓冲区准备就绪,fifo_request_drain() 将休眠直到有可用缓冲区。
-
从缓冲区读取后,以 req_bytes=1 调用 fifo_drained() 函数。
-
写入帧缓冲区的线程以相同方式调用 fifo_request_write() 和 fifo_wrote() 函数。
-
FIFO_BACKOFF 应设置为零。此功能对帧缓冲区无意义。
5.3 丢弃与重复帧
考虑一种情况:存在一个连续图像帧源,且数据消费者可能无法始终足够快地收集数据,因此必须避免溢出状态。
思路是防止传输数据的线程发生阻塞,该线程负责将数据从数据源传输到帧缓冲区。为此,应对每个传入帧循环执行以下序 列:
-
调用 fifo_request_write() 函数以确定写入哪个帧缓冲区
-
写入 info->position 指向的帧缓冲区
-
写入完成后,再次调用 fifo_request_write() 函数。此函数调用肯定不会阻塞,因为自上次调用以来,尚未报告有任何缓冲区 已被写入。
-
如果 fifo_request_write() 返回的值大于 1,则调用 fifo_wrote() 函数(当然,req_bytes=1)。随后对 fifo_request_write() 的 调用肯定不会阻塞,因为还有不止一个缓冲区可用,且仅消耗了一个。实际上,下一次对 fifo_request_write() 的调用可以通过直接选取下一 个帧缓冲区来代替。
-
另一方面,如果 fifo_request_write() 仅返回 1,则不调用 fifo_wrote()。相反,在下一个用于接收传入数据的循环中再次使用 当前缓冲区,或者直接将来自数据源的整个帧排空到无特定目的地。
由于这种用法防止了阻塞,可以删除 fifo_request_write() 实现中的 while() 循环,因为它永远不会被调用。还可以通过删除 相关的信号量及其初始化和销毁代码来进一步简化代码。将它们保留在代码中影响极小,因此这种优化主要是为了保持代码的可读性。
类似的方法可用于在从 FIFO 读取的线程上重复帧:在调用 fifo_drained() 函数之前再次调用 fifo_request_drain() 函数,如果 返回值小于 2,则重复当前帧。
