
1. 项目概述在RK3568平台上实现OSD叠加最近在折腾一块基于瑞芯微RK3568芯片的开发板客户提了个挺实际的需求要在实时视频画面上叠加一些自定义信息比如时间、通道号、设备名称甚至是一些简单的图形或告警标识。这其实就是我们常说的OSDOn-Screen Display功能。听起来简单不就是把一些文字画到屏幕上嘛但真要在嵌入式Linux特别是像RK3568这种集成了强大多媒体处理能力的平台上高效、稳定、低延迟地实现它里头的门道可不少。尤其是当你面对的是高帧率、高分辨率的视频流时如何不让OSD绘制成为性能瓶颈甚至影响主视频的编解码就成了关键。RK3568作为一款面向AIoT和边缘计算的中高端芯片其视频处理子系统VPU和显示控制器VOP能力都很强这为我们实现OSD提供了硬件加速的可能。但相应的软件架构和驱动层面的复杂度也上来了。你不能简单地在应用层用OpenCV画个图然后混合那CPU占用率立马飙升且延迟无法控制。这个项目的核心就是探索在RK3568的Linux SDK通常是基于Android或Buildroot环境下如何利用其硬件图层混合引擎实现一个高效的OSD叠加方案。这不仅仅是写个Demo而是要形成一个可产品化、可配置、资源消耗可控的解决方案。2. 核心需求与方案选型解析2.1 为什么需要硬件OSD在深入RK3568的具体实现前我们先搞清楚一个根本问题为什么非得用硬件方案软件绘制不行吗当然可以但代价很大。假设我们处理的是1080P30fps的视频流。如果我们在应用层对每一帧视频都用CPU进行解码后的YUV或RGB数据与OSD位图进行Alpha混合即使使用NEON指令集优化其计算量也是巨大的。这会持续占用可观的CPU资源导致系统整体响应变慢功耗增加并且在帧率很高时极易出现掉帧。更关键的是延迟软件处理的管道长从视频数据就绪到最终显示中间可能经历多次内存拷贝和CPU处理这对于需要实时监控或交互的场景是不可接受的。而硬件OSD其核心思想是将OSD作为一层独立的图像数据交由显示控制器或专用的叠加层硬件与视频层在扫描输出到屏幕之前进行混合。这个混合过程是硬件实时完成的几乎不占用CPU资源延迟极低通常在一行扫描时间内并且混合算法如Alpha混合、色彩键控也是硬件固化的效率极高。RK3568的显示子系统通常通过VOP驱动暴露给系统就支持多个图层的硬件混合这正是我们实现高性能OSD的基石。2.2 RK3568显示框架与图层管理RK3568的显示框架在Linux内核中主要由DRMDirect Rendering Manager和Rockchip自定义的VOPVideo Output Processor驱动构成。对上层应用而言最常用的接口是DRM。当然在Android系统下还会经过HWCHardware Composer等抽象层。简单来说DRM将显示硬件抽象为多个“平面”Plane。每个Plane可以理解为一个独立的图层。RK3568的VOP通常支持多个类型的PlanePrimary 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/KMSKernel 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_DRMyCONFIG_DRM_ROCKCHIPyCONFIG_ROCKCHIP_VOP2y(RK3568使用的是VOP2)CONFIG_DRM_DW_HDMI_ROCKCHIPy(如果你使用HDMI输出)CONFIG_DRM_PANEL_SIMPLEy等显示面板驱动确保这些配置被启用后重新编译内核并烧录到设备。启动后可以检查/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 Bufferfb_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的MppMedia Process Platform库并输出到DRM它内部也会申请Overlay Plane。你的OSD应用和视频播放应用是独立的进程它们需要“共享”有限的硬件Overlay资源。这通常需要系统级的协调或者约定俗成视频应用使用第一个OverlayOSD应用使用第二个。更复杂的系统可能会有一个显示合成管理器来统一分配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_W16)/(SRC_H16)不成比例硬件会进行拉伸缩放。确保这是你期望的效果。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而不是使用通用的桌面环境。这样能实现对硬件资源最精细、最高效的控制。