交换像素缓冲区

按照最初的设计,Linux 图形子系统对于在进程、设备和子系统之间共享像素缓冲区分配的支持非常有限。现代系统需要在所有这三类之间进行广泛的集成;本文档详细说明了应用程序和内核子系统应如何处理二维图像数据的这种共享。

本文档参考了 GPU 和显示设备的 DRM 子系统、媒体设备的 V4L2,以及用户空间支持的 Vulkan、EGL 和 Wayland,但是任何其他子系统也应遵循此设计和建议。

术语表

图像:

概念上是像素的二维数组。像素可以存储在一个或多个内存缓冲区中。具有像素的宽度和高度、像素格式和修饰符(隐式或显式)。

行:

沿单个 y 轴值的跨度,例如从坐标 (0,100) 到 (200,100)。

扫描线:

行的同义词。

列:

沿单个 x 轴值的跨度,例如从坐标 (100,0) 到 (100,100)。

内存缓冲区:

用于存储(部分)像素数据的内存块。具有以字节为单位的步幅和大小,并且在某些 API 中至少有一个句柄。可能包含一个或多个平面。

平面:

图像部分或全部颜色和 alpha 通道值的二维数组。

像素:

图片元素。具有由一个或多个颜色通道值定义的单个颜色值,例如 R、G 和 B,或 Y、Cb 和 Cr。也可能具有 alpha 值作为附加通道。

像素数据:

表示像素或图像的部分或全部颜色/alpha 通道值的字节或位。一个像素的数据可能分散在多个平面或内存缓冲区中,具体取决于格式和修饰符。

颜色值:

表示颜色的数字元组。元组中的每个元素都是颜色通道值。

颜色通道:

颜色模型中的一个维度。例如,RGB 模型具有通道 R、G 和 B。Alpha 通道有时也算作颜色通道。

像素格式:

描述像素数据如何表示像素的颜色和 alpha 值的说明。

修饰符:

描述像素数据如何在内存缓冲区中布局的说明。

alpha:

表示像素中颜色覆盖范围的值。有时也用于半透明。

步幅:

表示像素位置坐标和字节偏移值之间关系的值。通常用作垂直连续平铺块开头两个像素之间的字节偏移量。对于线性布局,垂直相邻的两个像素之间的字节偏移量。对于非线性格式,必须以一致的方式计算步幅,通常就像布局是线性的一样。

间距:

步幅的同义词。

格式和修饰符

每个缓冲区必须具有一个基础格式。此格式描述为每个像素提供的颜色值。尽管每个子系统都有自己的格式描述(例如 V4L2 和 fbdev),但应尽可能重用 DRM_FORMAT_* 令牌,因为它们是用于交换的标准描述。这些令牌在 drm_fourcc.h 文件中描述,该文件是 DRM uAPI 的一部分。

每个 DRM_FORMAT_* 令牌描述了图像中像素坐标与该像素的内存缓冲区中包含的颜色值之间的转换。描述了颜色通道的数量和类型:它们是 RGB 还是 YUV,整数还是浮点数,每个通道的大小及其在像素内存中的位置,以及颜色平面之间的关系。

例如,DRM_FORMAT_ARGB8888 描述了一种格式,其中每个像素在内存中具有单个 32 位值。Alpha、红色、绿色和蓝色颜色通道在每个通道上以 8 位精度提供,按照从高位到低位的顺序以小端存储。 DRM_FORMAT_* 不受 CPU 或设备字节序的影响;内存中的字节模式始终如格式定义中所述,通常为小端。

作为一个更复杂的示例,DRM_FORMAT_NV12 描述了一种格式,其中亮度分量和色度分量 YUV 样本存储在单独的平面中,其中色度平面以两个维度的一半分辨率存储(即,为每个 2x2 像素分组存储一个 U/V 色度样本)。

格式修饰符描述了这些每像素内存样本与缓冲区的实际内存存储之间的转换机制。最直接的修饰符是 DRM_FORMAT_MOD_LINEAR,它描述了一种方案,其中每个平面都按行顺序从左上角到右下角布局。这被认为是基线交换格式,并且对于 CPU 访问最方便。

现代硬件采用更复杂的访问机制,通常利用平铺访问,并且可能还使用压缩。例如,DRM_FORMAT_MOD_VIVANTE_TILED 修饰符描述了内存存储,其中像素存储在按行主序排列的 4x4 块中,即平面中的第一个平铺存储像素 (0,0) 到 (3,3)(包括),平面中的第二个平铺存储像素 (4,0) 到 (7,3)(包括)。

某些修饰符可能会修改图像所需的平面数量;例如,I915_FORMAT_MOD_Y_TILED_CCS 修饰符为 RGB 格式添加了第二个平面,其中存储有关每个平铺状态的数据,尤其包括该平铺是否完全填充了像素数据,或者是否可以从单个纯色扩展。

这些扩展布局高度依赖于供应商,甚至特定于每个供应商的特定代或设备配置。因此,所有用户必须显式枚举和协商对修饰符的支持,以确保兼容且最佳的管道,如下所述。

尺寸和大小

每个像素缓冲区必须附带逻辑像素尺寸。这指的是可以从基础内存存储中提取或存储到其中的唯一样本的数量。例如,即使 1920x1080 DRM_FORMAT_NV12 缓冲区具有一个包含 Y 分量的 1920x1080 样本的亮度平面,以及包含 U 和 V 分量的 960x540 样本,但整个缓冲区仍然描述为具有 1920x1080 的尺寸。

不保证缓冲区的内存中存储立即从基础内存的基地址开始,也不保证内存存储紧密地裁剪到任一维度。

因此,必须用字节为单位的 offset 来描述每个平面,在执行任何每像素计算之前,将该偏移量添加到内存存储的基地址。这可以用于将多个平面组合到单个内存缓冲区中;例如,DRM_FORMAT_NV12 可以存储在单个内存缓冲区中,其中亮度平面的存储紧接在缓冲区的开头以偏移量 0 开始,并且色度平面的存储在同一缓冲区中以该平面的字节偏移量开始。

每个平面还必须具有以字节为单位的 stride,表示两个连续行之间内存中的偏移量。例如,一个尺寸为 1000x1000 的 DRM_FORMAT_MOD_LINEAR 缓冲区可能已分配为好像它是 1024x1000,以便允许对齐的访问模式。在这种情况下,缓冲区仍将描述为宽度为 1000,但是步幅将为 1024 * bpp,表明在 x 轴的正极端有 24 个像素,其值并不重要。

还可以通过简单地分配比通常需要更大的区域来进一步在 y 维度中填充缓冲区。例如,许多媒体解码器无法原生输出高度为 1080 的缓冲区,而是需要 1088 像素的有效高度。在这种情况下,缓冲区将继续描述为高度为 1080,并且每个缓冲区的内存分配会增加以解决额外的填充。

枚举

像素缓冲区的每个用户都必须能够枚举一组支持的格式和修饰符,它们一起描述。在 KMS 中,这是通过每个 DRM 平面上的 IN_FORMATS 属性来实现的,该属性列出了支持的 DRM 格式以及每种格式支持的修饰符。在用户空间中,通过 EGL 的 EGL_EXT_image_dma_buf_import_modifiers 扩展入口点、Vulkan 的 VK_EXT_image_drm_format_modifier 扩展以及 Wayland 的 zwp_linux_dmabuf_v1 扩展来支持。

这些接口中的每一个都允许用户查询一组支持的格式+修饰符组合。

协商

用户空间有责任为其使用协商可接受的格式+修饰符组合。这是通过简单的列表交叉来执行的。例如,如果用户想要使用 Vulkan 渲染要在 KMS 平面上显示的图像,则必须

  • 查询 KMS 以获取给定平面的 IN_FORMATS 属性

  • 查询 Vulkan 以获取其物理设备支持的格式,确保传递与预期渲染用途相对应的 VkImageUsageFlagBitsVkImageCreateFlagBits

  • 对这些格式求交集以确定最合适的格式

  • 对于此格式,对 KMS 和 Vulkan 支持的修饰符列表求交集,以获得该格式可接受的修饰符的最终列表

必须针对所有用途执行此交集。例如,如果用户还希望将图像编码为视频流,则必须查询其打算用于编码的媒体 API,以获取其支持的修饰符集,并针对此列表进行额外的交集。

如果所有列表的交集为空列表,则无法以这种方式共享缓冲区,必须考虑替代策略(例如,使用 CPU 访问例程在不同用途之间复制数据,但会产生相应的性能成本)。

生成的修饰符列表是无序的;顺序无关紧要。

分配

一旦用户空间确定了合适的格式以及相应的可接受修饰符列表,它必须分配缓冲区。由于在内核或用户空间级别都没有通用的缓冲区分配接口可用,客户端会任意选择分配接口,例如 Vulkan、GBM 或媒体 API。

每个分配请求至少必须采用:像素格式、可接受的修饰符列表以及缓冲区的宽度和高度。每个 API 可能会以不同的方式扩展这组属性,例如允许在两个以上的维度中分配、预期的使用模式等。

分配缓冲区的组件将任意选择它认为所请求分配的“最佳”修饰符、所需的任何填充以及底层内存缓冲区的其他属性,例如它们是存储在系统还是设备特定的内存中,它们是否在物理上是连续的,以及它们的缓存模式。用户空间看不到内存缓冲区的这些属性,但是 dma-heaps API 正在努力解决这个问题。

分配后,客户端必须查询分配器以确定为缓冲区选择的实际修饰符,以及每个平面的偏移量和步幅。不允许分配器更改正在使用的格式,选择未在可接受列表中提供的修饰符,也不允许更改像素尺寸,但通过偏移量、步幅和大小表示的填充除外。

传递其他约束,例如步幅或偏移量的对齐方式,在特定内存区域内的放置等,不在 dma-buf 的范围之内,并且格式和修饰符令牌无法解决。

导入

要在不同的上下文、设备或子系统中使用缓冲区,用户将这些参数(格式、修饰符、宽度、高度以及每个平面的偏移量和步幅)传递给导入 API。

每个内存缓冲区都由一个缓冲区句柄引用,该句柄在图像中可能是唯一的或重复的。例如,DRM_FORMAT_NV12 缓冲区可以通过使用每个平面的偏移参数将亮度缓冲区和色度缓冲区合并到单个内存缓冲区中,或者它们可能是内存中完全独立的分配。因此,每个导入和分配 API 必须为每个平面提供单独的句柄。

每个内核子系统都有自己的缓冲区管理类型和接口。DRM 使用 GEM 缓冲区对象 (BO),V4L2 有自己的引用等。这些类型在上下文、进程、设备或子系统之间不可移植。

为了解决这个问题,dma-buf 句柄用作缓冲区的通用交换。特定于子系统的操作用于将本机缓冲区句柄导出到 dma-buf 文件描述符,并将这些文件描述符导入到本机缓冲区句柄中。dma-buf 文件描述符可以在上下文、进程、设备和子系统之间传输。

例如,Wayland 媒体播放器可以使用 V4L2 将视频帧解码为 DRM_FORMAT_NV12 缓冲区。这将导致用户从 V4L2 中取消排队两个内存平面(亮度和平度)。然后,这些平面将导出到每个平面一个 dma-buf 文件描述符,这些描述符随后与元数据(格式、修饰符、宽度、高度、每个平面的偏移量和步幅)一起发送到 Wayland 服务器。然后,Wayland 服务器将这些文件描述符作为 EGLImage 导入,以通过 EGL/OpenGL (ES) 使用,作为 VkImage 导入以通过 Vulkan 使用,或作为 KMS 帧缓冲区对象导入;这些导入操作中的每一个都将采用相同的元数据并将 dma-buf 文件描述符转换为其本机缓冲区句柄。

支持的修饰符的非空交集并不能保证导入将成功到所有消费者中;它们可能具有超出修饰符所暗示的必须满足的约束。

隐式修饰符

修饰符的概念晚于上面提到的所有子系统。因此,它已被追溯到所有这些 API 中,为了确保向后兼容性,需要支持尚不支持修饰符的驱动程序和用户空间。

例如,GBM 用于分配要在 EGL 用于渲染和 KMS 用于显示之间共享的缓冲区。它有两个用于分配缓冲区的入口点:gbm_bo_create,它仅采用格式、宽度、高度和用法令牌,以及 gbm_bo_create_with_modifiers,它使用修饰符列表对此进行扩展。

在后一种情况下,分配如上所述,提供了一个可接受的修饰符列表,实现可以从中选择(或者如果无法在这些约束内分配则失败)。在前一种情况下,未提供修饰符,GBM 实现必须自行选择最有可能的“最佳”布局。这种选择完全取决于实现:如果实现通过任何启发式方法认为这是一个好主意,则有些会在内部使用不可 CPU 访问的平铺布局。实现有责任确保此选择是适当的。

为了支持这种由于不了解修饰符而导致布局未知的情况,定义了一个特殊的 DRM_FORMAT_MOD_INVALID 令牌。此伪修饰符声明布局未知,并且驱动程序应使用自己的逻辑来确定底层布局可能是什么。

注意

DRM_FORMAT_MOD_INVALID 是一个非零值。修饰符值零是 DRM_FORMAT_MOD_LINEAR,它明确保证图像具有线性布局。应注意确保零作为默认值不会与没有修饰符或线性修饰符混淆。另请注意,在某些 API 中,无效的修饰符值是通过带外标志指定的,例如在 DRM_IOCTL_MODE_ADDFB2 中。

在以下四种情况下可以使用此令牌
  • 在枚举期间,接口可能会返回 DRM_FORMAT_MOD_INVALID,作为修饰符列表的唯一成员,以声明不支持显式修饰符,或者作为更大的列表的一部分,以声明可以使用隐式修饰符

  • 在分配期间,用户可能会提供 DRM_FORMAT_MOD_INVALID,作为修饰符列表的唯一成员(等效于完全不提供修饰符列表),以声明不支持显式修饰符并且不得使用,或者作为更大的列表的一部分,以声明可以使用隐式修饰符进行分配

  • 在分配后查询中,实现可能会返回 DRM_FORMAT_MOD_INVALID 作为分配的缓冲区的修饰符,以声明底层布局是实现定义的,并且没有可用的显式修饰符描述;根据上述规则,只有当用户将 DRM_FORMAT_MOD_INVALID 作为可接受修饰符列表的一部分包含在内,或者未提供列表时,才会返回此值

  • 在导入缓冲区时,用户可能会提供 DRM_FORMAT_MOD_INVALID 作为缓冲区修饰符(或不提供修饰符),以指示由于某种原因该修饰符未知;只有在缓冲区没有使用显式修饰符分配时,这才可接受

由此可知,对于任何单个缓冲区,由生产者和所有消费者形成的操作的完整链必须是完全隐式或完全显式的。例如,如果用户希望分配一个用于 GPU、显示和媒体之间的缓冲区,但媒体 API 不支持修饰符,则用户 **不得** 使用显式修饰符分配缓冲区并尝试将缓冲区导入到没有修饰符的媒体 API 中,而是使用隐式修饰符执行分配,或单独分配用于媒体使用的缓冲区并在两个缓冲区之间进行复制。

作为上述情况的一个例外,可以从隐式修饰符“升级”到显式修饰符。例如,如果使用 gbm_bo_create(不带修饰符)分配缓冲区,则用户可以使用 gbm_bo_get_modifier 查询修饰符,然后如果返回有效修饰符,则将此修饰符用作显式修饰符令牌。

当分配用于不同用户之间交换的缓冲区并且修饰符不可用时,强烈建议实现使用 DRM_FORMAT_MOD_LINEAR 进行分配,因为这是交换的通用基线。但是,不能保证这会导致正确解释缓冲区内容,因为隐式修饰符操作仍可能受驱动程序特定的启发式方法影响。

任何希望交换缓冲区的新用户 - 用户空间程序和协议、内核子系统等 - 必须通过用于内存平面的 dma-buf 文件描述符、用于描述格式的 DRM 格式令牌、用于描述内存布局的 DRM 格式修饰符、至少用于尺寸的宽度和高度以及至少每个内存平面的偏移量和步幅来提供互操作性。