5 循环帧缓冲区(Cyclic Frame Buffers)

5.1 引言

在某些应用中,特别是视频图像的实时处理中,通常需要维护多个缓冲区,每个缓冲区具有固定大小。在视频处理应用中, 每个这样的缓冲区包含一帧图像。这使得可以根据需要跳过帧或重复播放帧。

在帧采集(frame grabber)应用中,可以通过跳过一个或多个帧直到有空闲缓冲区来处理溢出(overflow)情况。例如,在 实时显示应用中,当显示窗口被移动或调整大小时可能发生此类溢出。像这样丢弃帧可以防止来自视频源的连续数据流中断,同时保持较小 的延迟。

在帧回放应用(例如显示实时输出的屏幕)中,当没有更新的帧可显示时,输出图像会重复。这解决了源(例如磁盘)暂时 停顿导致显示图像短暂冻结的情况。虽然不完全优雅,但总比流失去同步要好。在许多情况下,图像重复机制虽然有些粗糙,但能很好地克 服帧率差异,特别是当输出帧率远高于输入帧率时(例如 30 fps 到 60 fps)。

本节讨论如何修改第 4.5 段中介绍的 FIFO 演示应用程序,以管理此类缓冲区集合。

5.2 适配 FIFO 示例代码

维护一组循环帧缓冲区与维护 FIFO 之间存在相似性。实际上,如果 FIFO 中的每个字节代表一个帧缓冲区,那么 FIFO 中某 个字节的可读或可写状态就等同于整个帧缓冲区的可读或可写状态。

例如,假设有一个帧采集应用程序,分配了四个帧缓冲区用于存放接收到的图像数据。再假设设置了一个四字节的 FIFO 来 帮助管理这四个帧缓冲区,具体如下:

接收数据的线程从第一个帧缓冲区开始,然后以循环方式继续到下一个。在开始写入新帧缓冲区之前,该线程检查四字节 FIFO 是否未满(full)。在完成一个帧缓冲区后,如果 FIFO 未满,它向 FIFO 写入一个字节,然后进入下一个帧缓冲区。

消费图像数据的线程按相同顺序循环访问帧缓冲区。在尝试从新帧缓冲区读取之前,它检查四字节 FIFO 是否不为空 (empty)。当它完成一个帧缓冲区并准备进入下一个时,它从 FIFO 读取一个字节。

通过坚持这一约定,可以保证接收数据的线程永远不会覆盖尚未消费的帧缓冲区,并且消费线程永远不会尝试读取包含无效 数据的帧缓冲区。实际上,FIFO 中的字节数表示集合中有效帧缓冲区的数量。

请注意,写入和读取的字节值无关紧要,因此实际上不需要分配这四字节内存并在其中存储数据。只有 FIFO 的握手机制起 作用。

因此,第 4.7 段中概述的 FIFO API 可以直接采用:

  • 调用 fifo_init() 函数,并将 size 参数设置为帧缓冲区的数量(注意:size 可以是任意整 数)。fifo_init() 将为 FIFO 分配并锁定内存,该内存永远不会被使用(因为每个字节仅象征一个帧缓冲区)。这种内存浪费可以忽略不计, 但可以删除代码中的相关部分以避免将来混淆。

  • 调用 fifo_request_drain() 函数以获取要读取的帧缓冲区。info->position 将包含要使用的帧缓冲区的索引(编号从 0 开始)。如果 没有帧缓冲区就绪,fifo_request_drain() 将休眠直到有可用的。

  • 从缓冲区读取后,调用 fifo_drained() 函数,并设置 req_bytes=1。

  • 由写入帧缓冲区的线程以相同方式调用 fifo_request_write() 和 fifo_wrote() 函数。

  • FIFO_BACKOFF 应设置为零。此功能对于帧缓冲区没有意义。

5.3 丢弃与重复帧

考虑一个连续图像帧源的情况,该源绝不能达到溢出(overflow)状态,因为数据消费者可能不总是足够快地收集数 据。

其思想是防止在将数据从数据源传输到帧缓冲区的线程上发生阻塞。为实现这一点,对于每个传入的帧,应循环执行以下序 列:

  • 调用 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,则重复当前帧。