交换像素缓冲区

最初设计时,Linux 图形子系统对在进程、设备和子系统之间共享像素缓冲区分配的支持极为有限。现代系统要求这三类对象之间进行广泛的集成;本文详细介绍了应用程序和内核子系统应如何处理二维图像数据的这种共享。

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

术语表

图像(image):

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

行(row):

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

扫描行(scanline):

行的同义词。

列(column):

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

内存缓冲区(memory buffer):

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

平面(plane):

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

像素(pixel):

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

像素数据(pixel data):

表示像素或图像的颜色/alpha 通道部分或全部值的字节或比特。根据格式和修饰符的不同,单个像素的数据可能会分布在多个平面或内存缓冲区中。

颜色值(color value):

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

颜色通道(color channel):

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

像素格式(pixel format):

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

修饰符(modifier):

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

alpha:

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

跨距(stride):

表示像素位置坐标与字节偏移量之间关系的值。通常用作垂直连续拼块开始处两个像素之间的字节偏移量。对于线性布局,则是两个垂直相邻像素之间的字节偏移量。对于非线性格式,必须以一致的方式计算跨距,这通常假定布局为线性来进行。

间距(pitch):

跨距的同义词。

格式与修饰符

每个缓冲区都必须有一个底层格式。该格式描述了为每个像素提供的颜色值。尽管每个子系统都有自己的格式描述(例如 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 描述了一种格式,其中亮度(luma)和色度(chroma)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 格式修饰符、用于维度的至少宽度和高度,以及用于每个内存平面的至少偏移量和跨距。