ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

RGA图像处理:实宽高、虚宽高与对齐约束全解析

RGA图像处理:实宽高、虚宽高与对齐约束全解析 做图像处理的开发经常会在API文档里看到两个宽高一个是显示/内容用的叫实宽高一个是内存分配/硬件排布用的叫虚宽高。这个差异在RGARaster Graphic Acceleration光栅图形加速中尤其重要因为RGA是硬件加速模块对对齐约束极其敏感。我在调瑞芯微RGA时就曾因为搞混这两个概念图像花了一个多星期才恢复正常。这篇文章是RGA系列的第三篇前两篇我们聊了RGA的整体工作方式和一个基本绘制流程这一篇专门把“实宽高、虚宽高与对齐约束”背后的原理、字段含义、常见坑和排查手段讲清楚。适合刚接触RGA的入门者也适合已经写了很久RGA代码但偶尔被花屏困扰的开发者。1. 先从一次“花屏”说起两套宽高是怎么被逼出来的1.1 内存里的一行不等于图像的一行很多教程里画图像内存都画成一个规规矩矩的矩形每个像素挨着排放一行结束正好是下一行的开始。但真实系统中图像数据往往存放在一个更大的内存缓冲区内每一行之间可能有一小段“多余”的间隔字节。这个间隔从哪里来主要出自硬件和性能要求。DMA传输是按块发起的一次burst常见是32字节、64字节CPU的cache line通常是64字节如果每一行的首地址恰好是64的倍数读取效率会明显提升视频编解码硬件、显示控制器又经常要求宽度或地址对齐到16字节再加上YUV格式有奇偶行交错、多平面排列的问题UV平面的起始偏移也必须按对齐后的跨度来算。所以系统分配图像buffer时常常把每行的“跨度”放宽到对齐值而不是恰好等于图像宽度。这个放宽后的跨度就是“虚宽”为了对齐而多分配出来的那些行数就是“虚高”。图像真实显示需要的那部分尺寸则叫“实宽高”。拿一个最简单的例子NV12格式宽1920、高1080。如果不做对齐Y平面一行是1920字节1920刚好是16的倍数所以行跨度不变。但1080对齐到16是1088于是buffer分配出来的Y平面可能是1920×1088字节比“内容实际需要的高度”多出8行垃圾数据。这多出来的8行就是虚高。RGA去读这笔内存时如果按1080去读每一行就会漏掉最后几行Y数据如果按1088去读则必须明确告诉RGA跨行步长是多少。1.2 哪些场景最容易把两套宽高搞混实宽高和虚宽高只在“buffer容量大于实际显示内容”时才出现差异但这种差异几乎无处不在V4L2采集摄像头输出640×480底层mmap出来的buffer每行可能按特定字节数对齐bytesperline不等于width。解码器输出H.264/H.265解码后硬件解码器经常把buffer宽高对齐到16输出一个比原始尺寸大的缓冲区。后处理链路缩放、旋转、裁剪、去隔行中间buffer一旦做过对齐处理后面的模块就必须知道真实跨度。GPU贴图上传OpenGL里的GL_UNPACK_ALIGNMENT本质上就是告诉驱动“每一行图像数据对齐到几字节”换到RGA语境就是wstride。在这些场景里如果拿着“实际显示宽高”去当“内存排布宽高”使用去计算下一平面地址、去填RGA参数、去做memcpy结果一定是错的。尤其memcpy这种操作很多人会觉得“我拷贝width×height不就行了吗”一旦源buffer有stride padding这样拷出来的图像就是歪的。1.3 RGA里实宽高、虚宽高对应哪些字段在瑞芯微RGA的接口里定义图像源和目标区域的结构体通常是rga_buffer_t不同SDK版本可能写成rga_info_t或RGA_OPTIONS但字段语义高度相似。其中和宽高直接相关的字段如下字段含义例子1920×1080 NV12 bufferwidth实宽图像内容的实际宽度1920height实高图像内容的实际高度1080wstride虚宽内存中每行的跨度单位通常是像素1920hstride虚高内存缓冲区的总行数单位是行1088这里有一个非常容易踩的坑wstride的单位是“像素”不是“字节”。很多人算了半天字节对齐把一行需要多少字节填进wstride结果RGA按这个数去跨行偏差巨大。正确做法是把“每行字节跨度”除以“每像素字节数”换算成像素跨度。RGB888每像素3字节ARGB8888每像素4字节NV12的Y平面每像素1字节但UV平面需要单独处理。2. RGA结构体字段怎么填以缩放NV12为完整示例2.1 核心结构体速览写代码之前先把宽高相关的结构体字段列出来。下面是一段基于公开API语义的结构体示意不同芯片和SDK版本可能字段名不同但逻辑一致typedef struct rga_buffer { uint64_t phy_addr; // 物理地址或dma_fd uint64_t vir_addr; // 虚拟地址mmap后的CPU地址 uint32_t width; // 实宽 uint32_t height; // 实高 uint32_t wstride; // 虚宽行跨度 uint32_t hstride; // 虚高缓冲高度 uint32_t format; // RGA_FORMAT_NV12 / RGA_FORMAT_RGB888 等 } rga_buffer_t;实际使用时源buffer直接从解码器或采集器拿到的参数里把width、height、wstride、hstride原样拷过来即可。目标buffer则需要结合输出需求和对齐规则设置。2.2 一个标准的NV12缩放格式转换流程假设一个最常见的场景解码器输出NV12实际宽高是1920×1080但buffer对齐后hstride1088我要把它缩放成1280×720的RGB888输出。输出buffer由自己malloc每行按1280像素排列没有额外padding。rga_buffer_t src, dst; memset(src, 0, sizeof(src)); memset(dst, 0, sizeof(dst)); src.phy_addr decoder_fd; src.width 1920; src.height 1080; src.wstride 1920; src.hstride 1088; src.format RGA_FORMAT_NV12; dst.phy_addr output_fd; dst.width 1280; dst.height 720; dst.wstride 1280; dst.hstride 720; dst.format RGA_FORMAT_RGB888; int ret rga_blit(src, dst, NULL, NULL, NULL); if (ret ! RGA_SUCCESS) { printf(rga_blit failed: %d\n, ret); }这段代码最关键的两行是src.hstride1088和src.wstride1920。如果把hstride误填成1080RGA读取Y平面时会认为每一帧的内容高度只有1080行最后几行Y数据没有被纳入计算更严重的是UV平面NV12的UV平面对应高度是540但如果hstride填了1080RGA计算UV平面起始地址时就会整体偏移若干行结果图像下半部分颜色全是错的。这种错位用眼睛看像“颜色发绿发紫”和真的色偏很像不打印参数很难定位。2.3 填参数的顺序先实后虚不要偷懒我自己总结了一个三步法写RGA参数前先过一遍确定“图像内容”的实宽实高就是屏幕显示、算法处理、缩放目标真正关心的width和height。查“内存分配”时的实际跨度如果buffer来自解码器或采集器一定能从上层结构中拿到stride信息如果buffer是自己分配的要么严格按RGA对齐要求分配要么手动做align_up计算。校验format和实宽高是否匹配比如NV12的宽度必须是偶数高度也建议偶数否则RGA参数检查直接拒绝。这三步看着简单但大部分问题都出在第二步有人想当然认为“width等于wstride”一旦上层buffer做过对齐就翻车还有人把wstride和hstride对调导致RGA读每一行都偏几字节输出图像出现规律的斜向撕裂。3. 对齐约束为什么偏偏是16/32/64这种数字3.1 RGA需要对齐的两个层面对齐约束通常分成两类起始地址对齐和宽高/跨度对齐。起始地址对齐很好理解RGA内部DMA引擎搬运数据时是按固定块大小发起的比如一次burst读32字节或64字节。如果buffer起始地址没有对齐DMA引擎就需要额外的拼接处理性能变差某些硬件实现甚至直接拒绝运算。宽高/跨度对齐则是为了让每一行的起始地址都落在对齐边界上。举个例子如果wstride对齐到16像素而每像素4字节那么每行起始地址就天然对齐到64字节。我可以打个比方对齐就像搬家时把所有箱子统一打包成同一种尺寸搬运工装车时不用每个箱子都停下来量一遍。RGA这种硬件模块内部的行缓冲和像素流水线在设计时已经假定“每行起始地址满足对齐条件”一旦破坏这个假设轻则效率下降重则输出错乱。3.2 常见对齐粒度速查不同芯片、不同RGA版本的对齐要求可能有差异但表里的数值可以作为通用参考项目常见对齐要求buffer起始地址32字节或64字节对齐RGB格式宽度通常4字节对齐即可推荐16字节YUV420格式宽度推荐16字节对齐至少2像素对齐YUV422格式宽度推荐8字节对齐至少2像素对齐高度视格式而定NV12建议偶数旋转/镜像后的目标目标跨度也要参与对齐不能只对齐源图对齐计算的代码非常简单就是把一个整数上取整到指定倍数static inline int align_up(int val, int align) { return (val align - 1) ~(align - 1); }用这个函数把1080向上对齐到16结果就是1088(108015)~15 1088。用的时候要注意align必须是2的幂否则这个位运算公式不成立。3.3 不对齐会产生什么可见后果不对齐可能出现两种结局。第一种是RGA参数校验失败返回错误码上层及时发现并处理这种反而安全。第二种是RGA照常“工作”但输出图像出现行错位、绿边、右下角色偏甚至因为访问越界把别的内存写坏导致进程崩溃或系统花屏。最坑的是第二种因为不会报错。我之前在一个项目里看到图像最右侧有一条像撕裂一样的异常排查了两天后才发现是wstride填错了RGA读每一行都偏了两像素。这类问题用静态代码检查根本查不出来只能靠打印参数和内存dump来定位。4. 实战翻车现场三个最容易掉进去的宽高陷阱4.1 陷阱一用实际宽高代替strideNV12下半部分花屏这个坑我在前面已经提过但值得单独展开。现象很典型解码器输出的NV12帧实际宽高1920×1080buffer的hstride是1088。一开始填RGA参数时偷懒把src.hstride也填成1080运行后图像上方正常、下方明显偏色。原因不复杂RGA读取图像是按“行跨度”去跳转地址的如果跨度信息比真实内存跨度小读到的每一行都向下一行偏移了一部分累积到几十行之后Y平面末尾和UV平面的衔接就会错位。UV平面尤其倒霉因为NV12的U/V是交织存放在一个平面里的起始地址一旦算错整片颜色数据全部错位。// 错误写法无视hstride src.hstride src.height; // 1080 // 正确写法使用分配buffer时的虚拟高度 src.hstride 1088;如果你是手动一块一块拷贝图像也许感觉不到这个差异但只要交给RGA做缩放或裁剪硬件就会严格按照你给的wstride/hstride去跳地址地址步长和真实物理步长不一致输出必花。4.2 陷阱二旋转90度之后宽高还按原图填旋转可能是RGA里最反直觉的操作。假设源图是1920×1080旋转90度后输出内容应该是1080×1920。如果你只改了width和height忘记改dst.wstride和dst.hstrideRGA会按错误的跨度去写入目标buffer输出的图像像被横着拉伸一样比例完全不对。正确做法是宽高互换时虚宽虚高也跟着换int src_w 1920, src_h 1080; dst.width src_h; // 实宽变 1080 dst.height src_w; // 实高变 1920 dst.wstride align_up(src_h, 16); // 虚宽也要对齐 dst.hstride align_up(src_w, 16); // 虚高也要对齐从源图到目标图的旋转本质上是把源图像素点按坐标映射到目标坐标系里。RGA硬件在做映射时读源图按源图的wstride走写目标图按目标的wstride走两者是独立的。所以每次做旋转、镜像、裁剪的组合操作都要单独检查源和目标两套参数。4.3 陷阱三裁剪ROI时坐标偏移用的是实宽还是虚宽裁剪一块区域做局部放大在图像处理里也极其常见。但裁剪矩形里的x、y坐标对应的是图像逻辑坐标系不是内存字节偏移。想要定位到内存里第y行的地址必须用y * wstride而不是y * width。如果wstride大于width用width算出来的地址会每行都向右侧偏移越往下偏得越多。以NV12的Y平面为例uint8_t *y_plane base; uint8_t *row y_plane y * src.wstride x; // 错误示范把wstride写成width uint8_t *wrong_row y_plane y * src.width x;UV平面的偏移更麻烦NV12的UV平面每行字节数是Y平面的一半但它的行跨度仍然是wstride/2所以裁剪时U/V坐标x必须除以2y也要除以2再按wstride/2去定位。很多人只记住了Y平面的换算规则到了UV平面就用一样的公式结果裁出来的图像上半部分正常、下半部分颜色就乱套了。5. 排查问题三板斧参数打印、内存dump、最小复现5.1 每次都把RGA参数完整打出来不管问题看起来像不像宽高问题先把参数完整打出来。我写了一个小函数调试时直接调用排查效率提升非常明显void dump_rga_buffer(const char *name, const rga_buffer_t *b) { printf(%s: fd/phy%lx vir%p fmt0x%x w%u h%u wst%u hst%u\n, name, b-phy_addr, (void *)b-vir_addr, b-format, b-width, b-height, b-wstride, b-hstride); }拿到源和目标的dump后先目测“width是否等于wstride”和“height是否等于hstride”。如果上层传下来的buffer来源复杂比如说经过了多次复制每层可能都会改一遍stride所以最好在RGA调用前后各打一次。如果某个字段在调用过程中被库内部改写也能及时发现。5.2 用十六进制dump内存验证stride真假当你怀疑buffer的真实排布和参数不一致时最直接的办法是dump内存把第一行的前32字节和第二行的前32字节都打印出来比较“第二行首地址”是否等于“第一行首地址 wstride × 每像素字节数”。拿一个宽度16像素的NV12 Y平面来说第一行Y地址 base第二行Y地址应该 base wstride。如果dump出来发现第二行确实从base wstride开始说明stride没填错如果第二行从base width开始说明分配器实际没有做padding或者你在其他环节做了一次压缩拷贝。这种办法尤其适用于那些“参数看起来都对但图像就是错位”的疑难杂症。5.3 最小复现先跑纯RGB再跑YUV如果问题来来回回都定位不准我建议丢掉复杂的编解码链路先构造一个纯软件生成的小图比如64×64的RGB888铺满颜色渐变直接喂给RGA做缩放。RGB没有YUV那种多平面布局宽高映射最直观。如果RGB也乱问题一定出在RGA配置或格式设置上如果RGB正常、YUV乱那就是YUV的stride和平面偏移的问题。这种二分定位法比拿着1080p实拍数据干猜快太多。我在实际项目中用这个方法抓到过好几次“灵异现象”看起来像是RGA驱动有bug最后发现是上层结构体在某个被忽略的地方把宽高改成了另一种对齐方式。6. 不只RGA实宽虚宽这套概念到处都是6.1 V4L2的bytesperline摄像头用V4L2采集时struct v4l2_pix_format里有bytesperline字段表示一行图像占多少字节。很多代码只用了width和height忽略了bytesperline把mmap buffer直接填进RGA源地址结果一旦内核对每行做了pad图像就歪了。正确做法是利用bytesperline换算wstridestruct v4l2_pix_format *pf fmt.fmt.pix; rga_buffer_t src { .width pf-width, .height pf-height, .wstride pf-bytesperline / bpp, // 字节数换算成像素数 .hstride pf-height, };这里尤其要注意bpp每像素字节数。NV12格式的Y平面每像素1字节所以wstride可以直接填bytesperline但如果你拿的是RGB888bytesperline除以3才是像素跨度。6.2 FFmpeg的linesizeFFmpeg的AVFrame里width和height是真实图像尺寸linesize[0]是一行数据在内存中的跨度。很多做播放器的人把AVFrame喂给RGA之前都容易忽略linesize。正确做法是把linesize[0]除以每像素字节数后填到wstride而不是直接用width。如果是YUV420P注意linesize[1]和linesize[2]是UV平面的行跨度数值往往是width/2但也不一定严格相等还是要看具体分配器。这个细节在转码、弹幕渲染、视频特效叠加时特别坑因为AVFrame的data[0]、data[1]、data[2]三个平面都可能各自有其对齐规则。6.3 MPP解码输出的对齐尺寸使用瑞芯微MPP解码时解码后的buffer尺寸经常是硬件对齐过的。我遇到过这样的场景视频实际编码尺寸是1888×1064解码器输出的buffer却是1920×1088。如果用1920×1088直接当实宽高喂给RGA画面四周等于被放大了一圈内容比例就不对正确方式是把1888×1064填到width/height把1920/1088填到wstride/hstride。这个例子说明了一个重要的思维习惯从任何硬件模块拿到的buffer尺寸都要先问一句“这是实宽高还是虚宽高”。MPP、V4L2、RGA、GPU每个模块都有自己的一套对齐策略但它们之间协作时只认一种语义——你要作为中间人把它们翻译对。最后再说一个细节RGA的宽高字段在不同SDK版本里命名并不统一有的叫real_w/vir_w有的叫width/wstride但底层语义就是我上面讲的这套。改代码时先确认头文件里结构体注释管谁叫虚、谁叫实别凭记忆照抄。我把这些坑都踩过之后现在看任何图像buffer第一反应都是先问三件事实际宽高是多少缓冲区跨度是多少地址对齐到多少这三个数对不上后续全是白忙。这也是我做RGA开发以来最有价值的一条经验。
返回列表