交换像素缓冲区¶
最初设计时,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
描述了一种格式,其中亮度(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 以获取其物理设备支持的格式,确保传递与预期渲染用途相对应的
VkImageUsageFlagBits
和VkImageCreateFlagBits
对这些格式求交集以确定最合适的格式
对于此格式,对 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 格式修饰符来描述内存中的布局、至少宽度和高度用于尺寸,以及至少偏移量和步幅用于每个内存平面。