RK3568嵌入式Linux硬件OSD实现:基于DRM的高性能图层叠加方案
1. 项目概述:在RK3568平台上实现OSD叠加
最近在折腾一块基于瑞芯微RK3568芯片的开发板,客户提了个挺实际的需求:要在实时视频画面上叠加一些自定义信息,比如时间、通道号、设备名称,甚至是一些简单的图形或告警标识。这其实就是我们常说的OSD(On-Screen Display)功能。听起来简单,不就是把一些文字画到屏幕上嘛?但真要在嵌入式Linux,特别是像RK3568这种集成了强大多媒体处理能力的平台上,高效、稳定、低延迟地实现它,里头的门道可不少。尤其是当你面对的是高帧率、高分辨率的视频流时,如何不让OSD绘制成为性能瓶颈,甚至影响主视频的编解码,就成了关键。
RK3568作为一款面向AIoT和边缘计算的中高端芯片,其视频处理子系统(VPU)和显示控制器(VOP)能力都很强,这为我们实现OSD提供了硬件加速的可能。但相应的,软件架构和驱动层面的复杂度也上来了。你不能简单地在应用层用OpenCV画个图然后混合,那CPU占用率立马飙升,且延迟无法控制。这个项目的核心,就是探索在RK3568的Linux SDK(通常是基于Android或Buildroot)环境下,如何利用其硬件图层混合引擎,实现一个高效的OSD叠加方案。这不仅仅是写个Demo,而是要形成一个可产品化、可配置、资源消耗可控的解决方案。
2. 核心需求与方案选型解析
2.1 为什么需要硬件OSD?
在深入RK3568的具体实现前,我们先搞清楚一个根本问题:为什么非得用硬件方案?软件绘制不行吗?
当然可以,但代价很大。假设我们处理的是1080P@30fps的视频流。如果我们在应用层,对每一帧视频都用CPU进行解码后的YUV或RGB数据与OSD位图进行Alpha混合(即使使用NEON指令集优化),其计算量也是巨大的。这会持续占用可观的CPU资源,导致系统整体响应变慢,功耗增加,并且在帧率很高时极易出现掉帧。更关键的是延迟,软件处理的管道长,从视频数据就绪到最终显示,中间可能经历多次内存拷贝和CPU处理,这对于需要实时监控或交互的场景是不可接受的。
而硬件OSD,其核心思想是将OSD作为一层独立的图像数据,交由显示控制器(或专用的叠加层硬件)与视频层在扫描输出到屏幕之前进行混合。这个混合过程是硬件实时完成的,几乎不占用CPU资源,延迟极低(通常在一行扫描时间内),并且混合算法(如Alpha混合、色彩键控)也是硬件固化的,效率极高。RK3568的显示子系统(通常通过VOP驱动暴露给系统)就支持多个图层的硬件混合,这正是我们实现高性能OSD的基石。
2.2 RK3568显示框架与图层管理
RK3568的显示框架在Linux内核中主要由DRM(Direct Rendering Manager)和Rockchip自定义的VOP(Video Output Processor)驱动构成。对上层应用而言,最常用的接口是DRM。当然,在Android系统下,还会经过HWC(Hardware Composer)等抽象层。
简单来说,DRM将显示硬件抽象为多个“平面”(Plane)。每个Plane可以理解为一个独立的图层。RK3568的VOP通常支持多个类型的Plane:
- Primary Plane:主图层,通常用于显示最主要的内容,比如桌面合成器(Weston)的最终输出。
- Overlay Planes:叠加图层,专门用于显示像视频、OSD这类需要实时更新和混合的内容。Overlay Plane通常支持YUV和RGB格式,并且带有缩放、旋转和Alpha混合功能。
- Cursor Plane:光标图层,用于显示鼠标指针。
我们的OSD目标,就是申请一个或多个Overlay Plane,将我们生成的OSD图像(通常是ARGB8888格式,带透明度通道)提交到这个Plane上,并设置好其在屏幕上的位置、混合方式(如Pixel Alpha或Constant Alpha),剩下的混合和输出工作就完全由硬件接管。
方案选型上,我们主要有两条路径:
- 基于DRM原生接口:直接使用
libdrm库,在用户空间申请Frame Buffer,绘制OSD内容,然后通过DRM IOCTL提交给Overlay Plane。这种方式最直接,效率高,依赖少,但需要开发者对DRM/KMS(Kernel Mode Setting)模型有较深理解。 - 基于GStreamer等多媒体框架:利用GStreamer的
waylandsink、kmssink或Rockchip的私有插件(如rkximagesink),这些Sink内部通常也使用了DRM的Overlay功能。我们可以通过GStreamer的元数据(Meta)或自定义插件的方式注入OSD。这种方式更适合已经使用GStreamer作为媒体管道的项目,集成起来相对方便,但灵活性和极限性能可能稍逊于原生DRM。
考虑到项目的控制力和性能要求,我们将重点放在基于原生DRM接口的方案上,这也是最能体现RK3568硬件能力的做法。
3. 开发环境搭建与核心工具链
3.1 SDK与内核配置
工欲善其事,必先利其器。首先,你需要获取RK3568的官方Linux SDK。无论是从Rockchip官网还是你的板卡供应商那里,一个完整的SDK通常包含U-Boot、Kernel和Rootfs。
内核配置是关键。你必须确保内核编译时开启了DRM和Rockchip VOP驱动支持。通常需要检查以下配置:
CONFIG_DRM=yCONFIG_DRM_ROCKCHIP=yCONFIG_ROCKCHIP_VOP2=y(RK3568使用的是VOP2)CONFIG_DRM_DW_HDMI_ROCKCHIP=y(如果你使用HDMI输出)CONFIG_DRM_PANEL_SIMPLE=y等显示面板驱动
确保这些配置被启用后,重新编译内核并烧录到设备。启动后,可以检查/sys/class/drm/目录,你会看到类似card0这样的设备节点,以及其下的card0-DSI-1、card0-HDMI-A-1等连接器(Connector)和相关的plane子目录。通过cat /sys/kernel/debug/dri/0/state可以查看当前DRM的状态,包括各个plane的使用情况。
3.2 用户空间库准备
我们需要在目标板(RK3568)上安装或编译libdrm库。大多数Buildroot或Yocto生成的根文件系统已经包含了它。你可以通过find /usr -name “libdrm*.so*”来确认。如果使用SDK,通常可以在buildroot/output/target目录下找到。
为了测试和开发,我们还需要一个基本的图形绘制库。对于简单的文字和图形,libdrm本身不提供绘制功能。我们可以选择:
- Cairo:一个强大的2D图形库,支持矢量图形和文字渲染,功能全面但稍重。
- FreeType + 自行混合:使用FreeType渲染文字到位图,再与颜色图形混合。更轻量,控制更细。
- 简单的位图操作:如果OSD只是固定图标和数字,可以预先生成位图,在内存中直接操作像素。
为了平衡功能和复杂度,本项目选择Cairo作为绘制引擎。我们需要在开发板上安装libcairo及其依赖(如libpng, libpixman)。同样,可以通过交叉编译工具链为你的目标板编译Cairo。
注意:在资源极其受限的场景下,Cairo可能显得庞大。此时可以考虑预渲染字体为位图集(Bitmap Font),在应用层使用纯软件混合,但这会牺牲一些灵活性和增加CPU负载。需要根据实际项目权衡。
4. 基于DRM的硬件OSD实现详解
4.1 DRM设备初始化与资源获取
一切从打开DRM设备开始。我们的代码需要执行以下步骤:
- 打开设备:通常打开
/dev/drm/card0。 - 创建DUMB Buffer:我们需要一块内存来存储OSD图像数据。通过
DRM_IOCTL_MODE_CREATE_DUMB创建一个“笨”缓冲区。这种缓冲区由内核管理,但其物理内存是连续的,便于GPU或显示控制器直接访问。 - 映射Buffer到用户空间:获取DUMB Buffer的句柄和大小后,通过
DRM_IOCTL_MODE_MAP_DUMB和mmap系统调用,将这块内核内存映射到用户进程的虚拟地址空间。这样我们就可以在用户态直接读写图像数据了。 - 获取Connector和Encoder:遍历DRM的资源,找到当前已连接的显示输出(如HDMI),并获取其支持的模式列表。通常我们选择首选模式或一个指定的分辨率(如1920x1080)。
- 寻找可用的Overlay Plane:这是核心步骤。遍历所有的Plane,筛选出类型为
DRM_PLANE_TYPE_OVERLAY且支持我们所需像素格式(如DRM_FORMAT_ARGB8888或DRM_FORMAT_XRGB8888)的Plane。一个VOP可能有多个Overlay Plane,我们需要记录下它们的ID和能力(如支持的缩放、旋转、Alpha模式)。
// 伪代码逻辑示意 fd = open(“/dev/drm/card0”, O_RDWR); drmModeRes *res = drmModeGetResources(fd); // 查找已连接的connector并获取显示模式 drmModeConnector *conn = find_connected_connector(res); drmModeModeInfo *mode = conn->modes[0]; // 使用第一个模式 // 查找可用的Overlay Plane drmModePlaneRes *plane_res = drmModeGetPlaneResources(fd); for (int i = 0; i < plane_res->count_planes; i++) { drmModePlane *plane = drmModeGetPlane(fd, plane_res->planes[i]); if (plane->plane_type == DRM_PLANE_TYPE_OVERLAY) { // 检查格式支持 for (int j = 0; j < plane->count_formats; j++) { if (plane->formats[j] == DRM_FORMAT_ARGB8888) { overlay_plane_id = plane->plane_id; break; } } } drmModeFreePlane(plane); }4.2 OSD图像生成与Frame Buffer关联
获取到可写的内存映射区域(mmap返回的指针)后,我们就可以在这块内存上“作画”了。这里我们引入Cairo。
- 创建Cairo Surface:根据Buffer的地址、宽度、高度和颜色格式,创建一个Cairo的图像Surface。对于ARGB8888格式,对应Cairo的
CAIRO_FORMAT_ARGB32。cairo_surface_t *surface = cairo_image_surface_create_for_data( mapped_buffer, // mmap得到的内存指针 CAIRO_FORMAT_ARGB32, osd_width, osd_height, osd_stride // 通常等于 width * 4 (字节) ); cairo_t *cr = cairo_create(surface); - 绘制OSD内容:现在你可以像在普通画布上一样使用Cairo API进行绘制。设置颜色、画线、画矩形、填充、渲染文字等。
// 设置透明背景 cairo_set_source_rgba(cr, 0, 0, 0, 0); cairo_paint(cr); // 绘制一个半透明的红色矩形 cairo_set_source_rgba(cr, 1.0, 0, 0, 0.5); cairo_rectangle(cr, 10, 10, 200, 100); cairo_fill(cr); // 绘制白色文字 cairo_set_source_rgb(cr, 1.0, 1.0, 1.0); cairo_select_font_face(cr, “Sans”, CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_BOLD); cairo_set_font_size(cr, 24); cairo_move_to(cr, 20, 60); cairo_show_text(cr, “RK3568 OSD”); - 将DUMB Buffer转换为Frame Buffer:绘制完成后,我们需要让DRM知道这块内存可以作为Frame Buffer使用。通过
drmModeAddFB2API,将我们的DUMB Buffer句柄、宽度、高度、像素格式等信息注册进去,得到一个fb_id(帧缓冲区ID)。这个fb_id将在后续提交Plane时使用。uint32_t handles[4] = {dumb_handle, 0, 0, 0}; uint32_t pitches[4] = {osd_stride, 0, 0, 0}; uint32_t offsets[4] = {0, 0, 0, 0}; uint32_t fb_id; ret = drmModeAddFB2(fd, osd_width, osd_height, DRM_FORMAT_ARGB8888, handles, pitches, offsets, &fb_id, 0);
4.3 Plane属性设置与页面翻转(Page Flip)
有了fb_id,我们就可以配置Overlay Plane并将其显示到屏幕上了。在DRM Atomic API(现代推荐方式)中,我们需要设置Plane的一系列属性。
创建属性请求:使用
drmModeAtomicAlloc分配一个原子操作请求。设置关键属性:对于Overlay Plane,通常需要设置以下几个核心属性:
SRC_X/SRC_Y/SRC_W/SRC_H:指定Frame Buffer中的源矩形区域(单位是16.16定点数,即实际像素值左移16位)。通常我们使用整个Buffer,所以SRC_W/Height是osd_width << 16。CRTC_X/CRTC_Y/CRTC_W/CRTC_H:指定OSD图层在屏幕(CRTC)上的显示位置和大小。这决定了OSD叠加在屏幕的哪个区域。FB_ID:关联我们刚刚创建的帧缓冲区ID。CRTC_ID:关联到具体的显示控制器(CRTC),我们从之前找到的Connector对应的Encoder上获取。plane-type:通常不需要设置,创建时已确定。alpha:全局透明度属性(如果支持)。有些Plane支持pixel-alpha(每个像素自带Alpha)和plane-alpha(整个图层一个Alpha值)的组合。我们需要根据硬件能力和需求来设置。
drmModeAtomicAddProperty(req, plane_id, prop_src_x, 0); drmModeAtomicAddProperty(req, plane_id, prop_src_y, 0); drmModeAtomicAddProperty(req, plane_id, prop_src_w, osd_width << 16); drmModeAtomicAddProperty(req, plane_id, prop_src_h, osd_height << 16); drmModeAtomicAddProperty(req, plane_id, prop_crtc_x, screen_pos_x); drmModeAtomicAddProperty(req, plane_id, prop_crtc_y, screen_pos_y); drmModeAtomicAddProperty(req, plane_id, prop_crtc_w, display_width); drmModeAtomicAddProperty(req, plane_id, prop_crtc_h, display_height); drmModeAtomicAddProperty(req, plane_id, prop_fb_id, fb_id); drmModeAtomicAddProperty(req, plane_id, prop_crtc_id, crtc_id); // 设置整个图层的透明度,0xFFFF表示完全不透明,0x0000全透明 drmModeAtomicAddProperty(req, plane_id, prop_alpha, 0xFFFF);提交原子操作:调用
drmModeAtomicCommit提交所有属性的更改。使用DRM_MODE_ATOMIC_ALLOW_MODESET或DRM_MODE_ATOMIC_NONBLOCK等标志。一次成功的提交后,OSD就会立即显示在屏幕的指定位置。
动态更新:当OSD内容需要变化时(如时间更新),我们不需要重复创建Buffer和FB。只需:
- 在之前mmap得到的内存区域上,用Cairo绘制新的内容。
- 再次调用
drmModeAtomicCommit提交一次原子操作。即使FB_ID等属性没变,提交操作本身会触发硬件的重新扫描和混合。为了优化,可以只提交有变化的属性,但通常全量提交也很高效。
实操心得:在RK3568上,Overlay Plane对
SRC_W/H的设置非常敏感。务必确保(CRTC_W / CRTC_H) == (SRC_W >> 16) / (SRC_H >> 16),即显示宽高比与源数据宽高比严格一致,否则图像可能会被拉伸或显示异常。另外,SRC_W/H必须是16的整数倍,这是很多硬件Overlay的限制。
5. 性能优化与高级特性实现
5.1 双缓冲与撕裂避免
如果你需要频繁更新OSD(比如每秒更新一次时间),直接在前台Buffer上绘制然后提交,可能会遇到“撕裂”问题:即当显示器正在扫描读取Buffer数据时,你恰好更新了Buffer的内容,导致一帧内显示的数据来自更新前后两个版本。
解决方案是双缓冲(Double Buffering):
- 创建两个DUMB Buffer和对应的Frame Buffer(
fb_id_0,fb_id_1)。 - 在后台Buffer(比如Buffer B)上绘制新的OSD内容。
- 通过原子提交,将Plane的
FB_ID属性切换到指向Buffer B的fb_id。 - 下一轮更新时,在已经显示完毕的Buffer A(现在是后台)上绘制,再切换回来。
这样,显示硬件始终使用一个完整的、稳定的Buffer,而绘制操作在另一个Buffer上进行,避免了读写冲突。RK3568的DRM驱动对原子操作下的Buffer切换支持良好,可以实现无撕裂的更新。
5.2 多图层管理与Z序
RK3568的VOP2支持多个Overlay Plane。这意味着你可以实现更复杂的OSD效果,比如:
- 底层:实时视频。
- 中间层:半透明的信息面板。
- 顶层:高亮告警图标或闪烁的提示框。
实现多图层,就是为每个逻辑OSD层分配一个独立的Overlay Plane。每个Plane都有自己的FB_ID、位置、大小和Alpha属性。关键点在于Z序(堆叠顺序)。在DRM中,Plane的Z序通常由zpos属性控制。数值越大的Plane显示在越上面。你需要在原子操作中为每个Plane正确设置zpos属性。
// 假设plane_info_bg, plane_info_mid, plane_info_top是三个Overlay Plane的信息 drmModeAtomicAddProperty(req, plane_info_bg.id, prop_zpos, 0); drmModeAtomicAddProperty(req, plane_info_mid.id, prop_zpos, 1); drmModeAtomicAddProperty(req, plane_info_top.id, prop_zpos, 2);注意事项:硬件支持的Overlay Plane数量是有限的(例如RK3568可能支持2-4个)。在分配前,务必通过
drmModeGetPlane查询系统当前已使用的Plane,避免冲突。如果所有Overlay Plane都被占用(比如被视频播放器占用),你的OSD应用将无法获得硬件图层,需要回退到软件混合或与其他应用协商。
5.3 与视频播放的协同
在监控或多媒体设备中,OSD通常是叠加在解码后的视频流之上的。视频流本身也可能通过另一个Overlay Plane(或Primary Plane)显示。这时需要确保视频Plane和OSD Plane都正确设置,并且Z序关系正确(视频在下,OSD在上)。
如果视频解码使用了Rockchip的Mpp(Media Process Platform)库并输出到DRM,它内部也会申请Overlay Plane。你的OSD应用和视频播放应用是独立的进程,它们需要“共享”有限的硬件Overlay资源。这通常需要系统级的协调,或者约定俗成:视频应用使用第一个Overlay,OSD应用使用第二个。更复杂的系统可能会有一个显示合成管理器来统一分配Plane资源。
一个务实的做法是,在你的OSD应用中,先尝试获取所有可用的Overlay Plane信息,然后选择一个未被视频流常用格式(如NV12)支持的,或者通过尝试drmModeSetPlane看是否失败来判断资源冲突。
6. 常见问题排查与调试技巧
6.1 OSD不显示或花屏
这是最常见的问题。请按以下步骤排查:
- 检查DRM设备权限:确保运行OSD程序的用户有读写
/dev/drm/card0的权限。通常需要将用户加入video组。 - 确认Plane分配成功:在调用
drmModeAtomicCommit之前,打印出所有要设置的属性值,确保plane_id,crtc_id,fb_id都有效且非零。 - 验证Frame Buffer格式:确保
drmModeAddFB2使用的像素格式与Cairo Surface的格式、以及Plane支持的格式完全一致。ARGB8888在内存中的字节序可能是A、R、G、B,也可能是B、G、R、A(小端),需要和Cairo的CAIRO_FORMAT_ARGB32定义对齐。RK3568的DRM驱动通常期望的是ARGB8888(即32位,Alpha在最高字节)。 - 检查内存对齐和步长(Stride):创建DUMB Buffer时,其步长可能不等于
width * 4,因为内存控制器可能有对齐要求(如64字节对齐)。务必使用drmModeGetFB或创建Buffer后查询到的pitch作为步长传给Cairo和drmModeAddFB2。使用错误的步长是导致花屏的元凶之一。 - 查看内核日志:使用
dmesg | tail或journalctl -f查看内核DRM驱动的报错信息,常有奇效。常见的错误如“invalid pitch”、“unsupported pixel format”都会在这里打印。
6.2 OSD显示位置或大小错误
- 检查坐标和尺寸:确认
CRTC_X/Y没有超出屏幕范围,CRTC_W/H没有超过屏幕分辨率。同时确认SRC_W/H是osd_width/height << 16。 - 检查缩放:如果你设置的
CRTC_W/H与(SRC_W>>16)/(SRC_H>>16)不成比例,硬件会进行拉伸缩放。确保这是你期望的效果。 - Plane能力限制:有些Overlay Plane对最小/最大宽度、高度、位置对齐有要求(比如必须是2像素对齐)。通过
drmModeGetPlane获取的plane->formats属性列表可能附带modifier,但更详细的能力需要通过drmModeGetProperty和drmModeGetPropertyBlob来查询PLANE_PROP中的SRC_W/H等属性的最小/最大值。
6.3 性能问题:卡顿或CPU占用高
- 避免频繁的完整提交:如果OSD只有局部变化(如一个数字),可以只重绘变化部分,但提交操作仍然是全局的。确保你的绘制和提交循环没有不必要的阻塞。
- 检查绘制效率:Cairo绘制复杂路径或大量文字可能较慢。对于固定不变的OSD部分,可以预渲染到位图,每次只更新变化区域。使用
cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE)来快速清除区域,而不是用透明色重绘。 - 使用双缓冲:如5.1节所述,双缓冲能避免因绘制延迟导致的提交错过垂直同步,从而提升流畅度。
- 监视系统负载:使用
top或htop查看进程CPU占用。如果OSD进程占用持续很高,说明绘制是瓶颈。如果占用很低但依然卡顿,可能是DRM提交或硬件调度的问题。
6.4 与图形桌面环境(如Weston)共存
如果你的RK3568运行了Wayland合成器(如Weston),它通常会占用所有的DRM资源(包括Primary Plane和可能的Overlay Planes)。在这种情况下,你的独立OSD应用将无法直接操作DRM。
解决方案有两种:
- 通过Wayland协议:将你的OSD应用改造成一个Wayland客户端,创建一个透明(或指定区域透明)的Surface,由Weston负责合成。这失去了硬件Overlay的低延迟优势,但兼容性好。
- 修改或替换合成器:使用一个更简单的、允许其他应用共享Overlay Plane的DRM后端合成器,或者直接运行在没有桌面环境的纯控制台模式下。这是追求极致性能的嵌入式设备的常见选择。
我个人在RK3568上的实践是,对于专业的视频监控设备,通常会定制一个轻量级的显示服务,直接管理DRM和所有的图层(视频、OSD、UI),而不是使用通用的桌面环境。这样能实现对硬件资源最精细、最高效的控制。
