画家选纸,指腹一搓就知道纤维粗细。工程师选 Buffer,少算一个 Stride——一行像素就错位了。
本文不画山水,只算账:一张"数字宣纸"到底要多少内存、占多少带宽、跟硬件的脾气对不对得上。
画家选纸凭手感,工程选纸看账单。
你需要读过 01,或者至少知道 Frame Buffer 是一块存像素的内存。
引子:那张你没算清的宣纸
01 篇结尾,我们知道了一件事——"留白"靠 Alpha=0 的精确写入。但 Alpha 写到哪里?写到 Frame Buffer——那块被 01 叫做"宣纸"的内存。
上次我们提了生宣洇墨、熟宣不洇,提了 Stride 是隐藏的坑,提了 RGBA8888 的 4 字节贵但合规要求更贵。然后文章就结束了。
三个问题没来得及追问:
- 为什么行对齐偏偏是 128 字节而不是 64 或 256?
- RGB565 号称只需 4.1MB——真的只需 4.1MB 吗?还是说,这只是逻辑上的数字,物理上要多得多?
- 1080p 60Hz 的带宽账单,为什么会让一块 256MB 的嵌入式板子"窒息"?
本文是这些追问的回答。不画山水,只算账。
一、宣纸的"质地"——像素格式不是选择题,是约束方程
生宣之外的几十种纸
宣纸的"质地"不由纸的种类决定,而由纤维密度与帘纹走向决定。生宣纤维松散、帘纹粗疏——墨迹自然晕染,边界柔和;熟宣纤维致密、帘纹紧细——笔触边界锐利,墨不扩散。从最洇到完全不洇,中间是几十种纤维密度,每一种都改变了纸对墨的"承载方式"。
在嵌入式管线中,"墨分五色"并非写意挥洒——像素格式的通道布局与量化精度,就是工程师的五色配方。生宣/熟宣的纤维密度,对应内存的 Stride 对齐与 Cache 行边界。不存在"最好的"像素格式——每一种都是分辨率 × 色深 × Alpha精度 × 字节数 × 显示控制器兼容性这五个变量的约束方程。选格式不是挑纸的"手感",是解一道由五维约束构成的工程方程。
格式矩阵:每一种都有代价
| 格式 | BPP | 1080p 单帧 | Alpha 精度 | 适用场景 |
|---|---|---|---|---|
| RGBA8888 | 4 | 8.3 MB | 256 级 | 工业 HMI,合规对比度 |
| BGRA8888 | 4 | 8.3 MB | 256 级 | DRM/KMS 常见格式(受 SoC 字节序约束) |
| ARGB8888 | 4 | 8.3 MB | 256 级 | 部分 ARM Mali IP 偏好 |
| RGB565 | 2 | 4.1 MB | 无 | 预览流,非合规叠加 |
| ARGB1555 | 2 | 4.1 MB | 1 位(二值透明) | 简单掩膜,无半透 |
| A8(灰度) | 1 | 2.1 MB | 256 级(仅遮罩) | 单通道遮罩层 |
| NV12(YUV semi-planar) | 1.5 | 3.1 MB | 无 | 摄像头采集原生格式 |
| I420(YUV planar) | 1.5 | 3.1 MB | 无 | 同上,三平面布局 |
上表的真正用途不是"选一个",而是让你意识到:当你选择 RGB565 省下 4.2MB 内存时,你同时放弃了 Alpha 通道——"留白"这个动作在像素层面消失了。半透明热力图、报警红框的边缘过渡、OSD 文字后的暗色遮罩——这些全部需要 Alpha,而 RGB565 没有。
很多人问:那我用 RGB565 + 透明色掩码(color key)不行吗?告诉你一个"透明绿幕"是哪来的:把某个 RGB 值(比如品红 #FF00FF)定义为"透明色"。结果是——报警图标的红色边缘和透明色掩码的色相在低精度量化下混在一起,图标边缘发虚。不是算法不行,是画布从底层就放弃了"留白的精确权"。
字节序:RGBA 写成 BGRA,红框变蓝框
一个更隐秘的陷阱——
RGBA8888 的内存布局: [R][G][B][A] ← 人类直觉:先红后绿后蓝 BGRA8888 的内存布局: [B][G][R][A] ← 小端 SoC + DRM `drm_fourcc.h` 中 `DRM_FORMAT_BGRA8888` 的常见硬件管线预设 ARGB8888 的内存布局: [A][R][G][B] ← 大端 SoC 或某些 ARM Mali IP 的内部布局
如果渲染管线的输出格式是 RGBA8888,但 DRM-KMS 期望 BGRA8888,画面不会黑屏,而是红蓝通道互换。那条给操作员看的报警红框会变成报警蓝框——画面还在,语义全错。
这不是"驱动没写好",是隐式的格式转换没做。而做这个转换的代价是什么?
RGBA8888 → BGRA8888 通道交换: 标量 C 逐字节交换: 每个像素 4 次读+4 次写,1920×1080 = 830 万次操作 NEON 向量化 (vld4+vswp+vst4):一次处理 8 个像素,吞吐约 850 MB/s(Cortex-A7 1GHz, NEON, gcc 10.3 -O2;完整环境声明见第五节)
在 Cortex-A7 1GHz(gcc 10.3 -O2)上,标量版本约吃 18% CPU(全帧),NEON 优化后约 5%(完整测试环境声明见第五节)。但无论哪种优化,都有一个本不该存在的格式转换在消耗你的帧预算。
原则只有一条:渲染格式应匹配显示控制器的硬件管线预设。 DRM/KMS 通过 drm_fourcc.h 注册像素格式(如 DRM_FORMAT_BGRA8888、DRM_FORMAT_XRGB8888),具体布局取决于 SoC 字节序(Little/Big Endian)与显示控制器的硬件像素引擎——不存在跨平台的"默认格式"。如果目标 SoC 的 DPU 硬件合成器消费 BGRA8888,那就用 BGRA8888 渲染——不要让兼容性补贴变成账单上的固定开销。
代码:"生宣"与"熟宣"的选择
// 生宣路径(洇墨):RGBA→BGRA 需要额外通道交换,每帧多一次 pass // 熟宣路径(不洇):输出即 SoC 硬件管线预设格式,零转换 typedef struct { uint8_t b, g, r, a; } pixel_bgra_t; // 熟宣:DPU 硬件直认 typedef struct { uint8_t r, g, b, a; } pixel_rgba_t; // 生宣:需要通道交换 // DRM 通过 drm_fourcc.h 注册格式;目标 SoC 的 DPU 消费 BGRA8888 // 不是"改成 BGRA 更好",是"不改成 BGRA,红蓝就互换了" pixel_bgra_t *fb = drm_dumb_mmap(fd, &map); // FB 格式与 DPU 预设一致 // 直接写入:红框画出来就是红框,不会变成蓝框 fb[p].r = 255; fb[p].g = 0; fb[p].b = 0; fb[p].a = 255;
✅ 可复现验证:用
modetest -M <driver> -c查看 DRM connector 汇报的FORMATS列表(原生支持的像素格式集合)。在/sys/kernel/debug/dri/0/state中对比FB_ID.format和 Connector 期望值是否一致。
二、宣纸的"帘纹"——Stride 不是 bug,是物理定律
对着光看到的纹理
手工宣纸对着光看,能见到一道道横向的"帘纹"——竹帘抄纸时纤维留下的纹理。画师运笔越过一道帘纹时,笔触会有微妙变化——纤维走向不同,吸墨速度不同。画了几十年的老画师,磨墨的时候就考虑到了纸的帘纹方向。
Stride 就是 Frame Buffer 的"帘纹"。它不是你代码里的 bug,是硬件的物理约束。01 篇用一个段落提了 Stride,现在把它拆开来看。
三个物理原因,解释一个对齐
第一,Cache line 边界。 ARM Cortex-A7 的 cache line = 64 字节。如果你一行像素从 cache line 中间开始,CPU 一次 cache fill 只能覆盖半行。下一行接着从不对齐的地址开始,cache miss 率翻倍——一行错,行行错。这不是"慢一点",而是同样的渲染操作突然多了近一倍的 DDR 访问。
第二,DMA burst 传输。 DDR 控制器以"burst"为单位搬运数据,一次 burst 典型长度 = 8 拍(8 × 数据总线宽度)。如果目标地址不对齐 burst 边界,DDR 控制器要把第一次 burst 拆成两次——多一次握手、多一次等待、多几十个纳秒。一帧有 1080 行,每行都多一次额外的 burst,33.3ms 的帧预算就这样被吃掉几个毫秒。
第三,显示控制器扫描。 LCD 控制器是逐行扫描的硬件状态机。它不关心你的"逻辑宽度",它只按寄存器里配的 stride 值读下一行。如果你往寄存器里写了 7680,但实际 buffer 按 7936 分配——控制器每行都读偏一点,偏到第 1080 行时画面彻底错位。
不同 SoC 的对齐要求——没有"通用"的 Stride
| 平台 | 显示控制器 | 对齐要求 | 1080p RGBA8888 实际 Stride | 备注 |
|---|---|---|---|---|
| RK3566 (G52) | DRM atomic | 64 字节 | ALIGN(7680,64) = 7680 ✅ | 恰好整除 |
| i.MX6ULL (PXP) | FBDEV | 32 字节 | ALIGN(7680,32) = 7680 ✅ | 恰好整除 |
| Allwinner V3s | DE-BE | 16 字节 | ALIGN(7680,16) = 7680 ✅ | 恰好整除 |
| 老式 ARM9 屏驱(ILI9xxx) | LCD 控制器 | 1024 字节 | ALIGN(7680,1024) = 8192 ⚠️ | 多了 512 |
| x86 (i915) | DRM modern | 64-128 字节 | ALIGN(7680,128) = 7808 ⚠️ | 01 的例子 |
关键数字:1080p RGBA8888,逻辑行宽 1920 × 4 = 7680。7680 ÷ 64 = 120,在要求 64 字节对齐的平台上恰好整除。这不是运气——显示控制器厂商特意选了对齐值使得 1920 宽度的 RGBA 格式能刚好卡在边界上。
但如果你遇到一块老式 ARM9 屏驱,其内部行缓冲(Line Buffer)固定为 1024 字节呢? 7680 ÷ 1024 = 7.5,无法整除。控制器会强制将 stride 上取整到 8 × 1024 = 8192。每行凭空多出 512 字节填充,1080 行就多了 552,960 字节——你的 buffer 分配如果是按逻辑宽度 7680 算的,末尾 553KB 的写入会越界到不相干的内存区域。
真实工程中,Stride 的值要从 DRM 的 dumb buffer 分配结果中读回来——最安全的做法不是算,是问内核:
struct drm_mode_create_dumb create = { .width = 1920, .height = 1080, .bpp = 32 }; ioctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &create); printf("内核返回的 pitch (stride): %d\n", create.pitch); // 不要自己算。读回来多少,就用多少。
Stride 不对齐的三种破坏
| 破坏模式 | 现象 | 根因 |
|---|---|---|
| 画面水平错位 | 每行偏移递增,画面逐行向右"漂移" | 读写 stride 不一致 |
| "鬼影像素" | 留白处出现上一帧的残留像素 | memset 只清到 width*h*bpp,没清到实际 buffer 尾 |
| 底部噪点带 | 画面底部出现随机色块 | LCD 控制器按 stride 读越界,读到了相邻内存区 |
中间那种最隐蔽——memset(canvas, 0, width * height * 4) 清的是逻辑大小,而 buffer 实际分配是按 stride 来的。末尾那几百 KB 没清零,填充字节里残留的旧像素数据在下一次被 LCD 控制器扫到时,就成了留白处的"鬼影像素"。
正确的清零写法:
// ❌ 危险:逻辑宽度清零——填充字节未覆盖 memset(canvas, 0, width * height * bpp); // ✅ 正确:实际步长清零——覆盖全部 buffer,包括对齐填充 memset(canvas, 0, height * stride);
可跑的 Stride 计算验证程序
// stride_calc.c — 在任何 Linux PC 上编译运行 // gcc -O2 -o stride_calc stride_calc.c && ./stride_calc #include <stdio.h> static int align_up(int x, int a) { return (x + a - 1) / a * a; } int main() { int w = 1920, bpp = 4, h = 1080; int logical = w * bpp; // 7680 printf("=== Stride 计算对比 ===\n"); printf("逻辑行宽: %5d bytes\n", logical); printf("64B 对齐 (主流 SoC): %5d bytes %s\n", align_up(logical, 64), align_up(logical, 64) == logical ? "恰好整除" : "⚠️ 有填充"); printf("128B 对齐 (x86 DRM): %5d bytes %s\n", align_up(logical, 128), align_up(logical, 128) == logical ? "恰好整除" : "⚠️ 有填充"); printf("256B 对齐 (老式 ARM9):%5d bytes %s\n", align_up(logical, 256), align_up(logical, 256) == logical ? "恰好整除" : "⚠️ 有填充"); int buf_logical = logical * h; int buf_aligned = align_up(logical, 128) * h; printf("\n=== 内存分配对比 ===\n"); printf("按逻辑行宽分配: %d bytes (%.1f MB)\n", buf_logical, buf_logical / (1024.0 * 1024.0)); printf("按对齐步长分配: %d bytes (%.1f MB)\n", buf_aligned, buf_aligned / (1024.0 * 1024.0)); printf("差额: %d bytes ← memset(width*h*bpp) 漏掉的\n", buf_aligned - buf_logical); return 0; }
✅ 可复现验证:上述程序在任何 Linux 环境下编译运行。在目标板上,运行
drm_info | grep pitch或cat /sys/kernel/debug/dri/0/framebuffer查看当前实际 Stride 值,与计算结果对比。
三、宣纸的"尺幅"——分辨率、帧率、带宽的三维方程
四尺宣画扇面,丈二匹画通景
四尺宣画扇面小品,六尺宣画中堂立轴,丈二匹画通景屏风。画家选纸的大小,不是"越大越好"——取决于画什么、挂哪面墙、从多远看。
嵌入式选分辨率和帧率,遵循同一套逻辑——不是"越大越好",是算清了带宽账单之后的最优解。
显示带宽方程
显示刷新带宽 = 分辨率宽 × 分辨率高 × 字节/像素 × 刷新率 = W × H × BPP × Hz
代入几个典型配置:
| 分辨率 | 格式 | 帧率 | 显示带宽 | 备注 |
|---|---|---|---|---|
| 720p | RGB565 | 30 Hz | 55 MB/s | 最省 |
| 720p | RGBA8888 | 30 Hz | 110 MB/s | |
| 1080p | RGB565 | 30 Hz | 124 MB/s | |
| 1080p | RGBA8888 | 30 Hz | 249 MB/s | 01 篇基准 |
| 1080p | RGBA8888 | 60 Hz | 498 MB/s | |
| 4K | RGBA8888 | 30 Hz | 996 MB/s | DDR3 扛不住 |
注意——这个 249 MB/s 只是显示控制器从 DDR 读 Frame Buffer 刷新屏幕的带宽。它只是带宽账单里的一行。
工程账单从不按理想值结算。 真实场景中,需计入三个隐藏系数:
实际带宽 ≈ (Stride 对齐后行宽 × 高 × BPP) × 缓冲数 × 刷新率 × 读写系数 以 1080p RGBA8888 + 128 字节对齐 + 双缓冲 + GUI 合成中间缓冲为例: 行宽 = ALIGN(1920×4, 128) = 7808 字节(理想值 7680) 实际 ≈ 7808 × 1080 × 3 × 30 × 1.5 ≈ 1.14 GB/s
对比理想值 249 MB/s,差了近 5 倍。多数入门级嵌入式 SoC 的 DDR 控制器可用带宽仅 1.5~2.0 GB/s——仅显示子系统就吃掉一半以上。当采集、AI 推理同时抢占总线,"窒息"的不是内存容量,是带宽管道被占满。
谁在和你抢 DDR 总线
同一块 DDR3 上,同时跑着这些消费者:
| 带宽消费者 | 1080p 30fps 典型消耗 | 说明 |
|---|---|---|
| 显示控制器(读 FB 送屏) | ~249 MB/s | 不可压缩 |
| 摄像头采集写入 | ~165 MB/s | YUV 422, 1920×1080@30fps |
| GUI 软件混合读写 | ~130 MB/s | 01 篇 perf 实测 |
| AI 推理张量读写 | ~500 MB/s | 轻量检测模型,INT8 量化 |
| CPU 取指/栈/堆 | ~200 MB/s | 操作系统 + 应用逻辑 |
| DDR 刷新开销 | ~50 MB/s | 物理定律,不可消除 |
| 合计(并发最坏情况) | ~1294 MB/s |
DDR3-1600 单通道理论带宽约 5000 MB/s,工程可用按 70% 计算 ≈ 3500 MB/s。1294 MB/s 看起来离天花板很远。
但瓶颈不是带宽总量,是总线仲裁的延迟抖动。
AI 推理帧到来时,NPU/CPU 发起连续 DDR burst 抢占总线。此时 GUI 混合操作里的 memcpy 被 DMA 控制器往后排——等了几个微秒才拿到总线。1280 行混合操作执行到第 800 行时,VSync 中断来了。Back Buffer 还有 280 行没画完。Page Flip 不能做——做了就是半帧新画面 + 半帧旧画面的撕裂。
01 篇说的"锁 30Hz 不是慢,是确定性",在带宽维度上有了更深的意义:每个 33.3ms 帧窗口内,渲染操作有固定时间预算。带宽不是不够,是总线忙的时候你排不上队。
三个场景的配置抉择
| 场景 | 推荐配置 | 决策逻辑 |
|---|---|---|
| 工业 HMI 单屏 | 1080p + RGBA8888 + 30Hz + 双缓冲 | 合规优先:报警色环 4.5:1 对比度不能降 |
| 多通道 NVR 预览 | 4×720p + RGB565 + 15Hz + 单缓冲 | 通道数优先:4 画面密度优先于单画面色彩精度 |
| 电池供电巡检仪 | 720p + RGB565 + 15Hz + Dirty Rect | 功耗优先:每少一帧多跑一小时 |
这三行不只是"建议",要理解背后的工程约束方程:
场景 1(HMI) :如果比较"RGBA8888 30Hz vs RGB565 60Hz",答案是前者。因为合规标准(ISA-101 / WCAG 2.1 AA 对比度)不可妥协——报警色环的红色和橙色在 RGB565 的 6.5 万色里拉不开。60Hz 的动效在报警状态变化中没有感知增益——报警框要么在,要么不在,不需要平滑过渡动画。
场景 2(NVR) :4 通道 D1 或 720p 拼接一个屏幕。每个子画面的单帧尺寸小,色彩要求低(摄像头预览不需要 Alpha 通道,也不需要 1670 万色验证合规对比度)。RGB565 + 15Hz 对每个子画面足够——检测通道数和覆盖范围是第一优先级。
场景 3(巡检仪) :电池供电,屏幕只在操作员按按钮时点亮。平时海量待机。15Hz 刷新 + Dirty Rect(只更新时间戳和检测框角标)将 DDR 访问降到最低——DDR 功耗与吞吐量正相关,越少搬运,越省电。
这三条没有"最佳配置"——只有"在特定约束下哪个参数可以妥协、哪个不能"。选之前先问:什么不可降?是分辨率?是色彩精度?是刷新率?还是通道数?答案决定了你该往哪个方向降。
为什么不是"加一块 NPU 专用 SRAM 就把问题解决了"
因为成本。三四美元的专用 SRAM 芯片 + PCB 面积 + 布线复杂度,让一块低端 MPU 的 BOM 成本上升 20-30%。在年出货量几十万到百万片的工业设备里,这不是"加不加"的问题——是"能不能过采购审"的问题。工程设计从来不是"能不能做",是"值不值得做"。
四、"不换纸"——DMA-BUF 与零拷贝路径
十几道工序,同一张纸
画师在熟宣上画工笔——勾线、晕染、罩染、醒线,十几道工序全在同一张纸上。他不撕、不换、不重贴。因为纸吃透了每一层墨,层与层之间的透叠不需要"重新开始"。
嵌入式的"换纸"是什么?是 memcpy。
拷贝路径:每一层都在换纸
V4L2 采集 Buffer ──[memcpy]──> 用户态临时 Buffer ──[memcpy]──> GUI Back Buffer ──[memcpy]──> 显示 Front Buffer
每次 memcpy 把同一份像素数据在 DDR 中搬运一遍。三次拷贝下来,8.3MB 的像素搬了 25MB——而内容没变,只是"位置的搬运"。
在 DDR3 平台上,memcpy 8.3MB 耗时约 7ms(按 1200 MB/s 实测吞吐)。三次拷贝 = 21ms。而你的帧预算总共只有 33.3ms。拷贝就吃掉 21ms——剩下的 12 毫秒留给 Alpha 混合和光栅化,够吗?
DMA-BUF 零拷贝:页表重映射,不动像素
V4L2 dmabuf fd ──[dma_fence 同步]──> DRM import ──[mmap]──> GUI 直接写入 ──[atomic commit]──> 显示
dma_buf_fd() 传递一个文件描述符,内核在页表上把同一块物理内存映射进两个设备的地址空间。像素不动,页表改。
关键 API 链路:
// 1. V4L2 侧:导出 dmabuf fd struct v4l2_exportbuffer exp = { .type = V4L2_BUF_TYPE_VIDEO_CAPTURE, .index = buf_index, .fd = -1 }; ioctl(v4l2_fd, VIDIOC_EXPBUF, &exp); int dmabuf_fd = exp.fd; // 这就是"宣纸的纤维"——物理内存的句柄 // 2. DRM 侧:import dmabuf 为 GEM 对象 struct drm_prime_handle prime = { .fd = dmabuf_fd, .handle = 0 }; ioctl(drm_fd, DRM_IOCTL_PRIME_FD_TO_HANDLE, &prime); // 3. 将 import 的 GEM 对象绑定为 DRM FB uint32_t pitches[4] = { stride, 0, 0, 0 }; uint32_t offsets[4] = { 0, 0, 0, 0 }; drmModeAddFB2(drm_fd, 1920, 1080, DRM_FORMAT_BGRA8888, &prime.handle, pitches, offsets, &fb_id, 0); // 4. GUI 侧:mmap dmabuf 直接写 void *pixels = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, dmabuf_fd, 0); // 直接渲染到这片"宣纸",没有任何 memcpy // 渲染完 → atomic commit → 显示控制器直接读同一块物理内存
从三次 memcpy 到三次 ioctl + 一次 mmap——这就是"不换纸"的工程现实。
零拷贝不是免费的
DMA-BUF 有几个硬性前提:
- 内核 ≥ 4.x 且 DRM 驱动支持
DRM_PRIME。不支持的 SoC,这条路根本不存在
- 连续物理内存:dmabuf 底层需要 CMA(Contiguous Memory Allocator)或 IOMMU + scatter-gather DMA。如果 SoC 没有 IOMMU,dmabuf 只能从 CMA 池分配——而 CMA 池大小通常只有几十 MB,在 bootloader 里配
- dma_fence 跨设备同步:V4L2 还在写、DRM 就要读——需要 fence 保证不会读到半帧。这需要内核 5.x+ 的 implicit fencing 或 explicit sync
所以零拷贝路径的选择,实际上是由 SoC 决定的。如果你的目标平台是 RK3566,它支持 DRM_PRIME + IOMMU——零拷贝可行。如果是一块老式 ARM9 + FBDEV——这条路不存在。三条拷贝,一条都不能少。
这就是"纸也选画家"的含义。 画师可以用生宣画大写意,也可以换熟宣画工笔。但如果你只有一块老式 ARM9 的"生宣"——你就只能在拷贝路径上做好优化:用 NEON 加速 memcpy、用 Dirty Rect 减少搬运面积、用更低的帧率换回带宽空间。
核心金句
"画家选纸,纸也选画家。工程师选格式,SoC 也选工程师。"
✅ 可复现验证:在支持 DRM_PRIME 的目标板上,启用
CONFIG_DMA_BUF和CONFIG_DRM_PRIME,用drm_info检查 connector capabilities 中的DRM_MODE_CONNECTOR_HDMIA是否包含 export/import 标记。用trace-cmd record -e dma_fence追踪跨设备 fence 同步耗时。
五、工程验证——画家看墨韵,工程师看火焰图
前面四层用宣纸的隐喻拆解了像素格式、Stride、带宽方程、零拷贝。但类比只是降低门槛的梯子。梯子用完,你要看到的是数据。
测试环境
- 芯片:Cortex-A7 1GHz(ARMv7,无硬件 GPU 加速)
- 内存:256MB DDR3(128MB 供系统 + 128MB 供 GUI 帧缓冲)
- 内核:Linux 5.10 + DRM-KMS
- 编译器:gcc 10.3,
-O2 -march=armv7-a -mfpu=neon
- 压测命令:
perf stat -e cpu-cycles,instructions,cache-misses,cache-references ./render_test --frames=300
新增基准数据(聚焦 Stride + 格式转换开销)
| 测试项 | 吞吐 MB/s | cache-miss% | CPU% | 关键发现 |
|---|---|---|---|---|
| memcpy 整帧(Stride 7680, 64B 对齐) | 1200 | 0.3% | 2.1% | 基线:对齐 = 高效 |
| memcpy 整帧(Stride 7936, 64B 对齐但多填充) | 1180 | 0.4% | 2.3% | 填充字节不参与 memcpy,开销来自多循环次数 |
| RGBA→BGRA 通道交换(NEON vld4+vswp+vst4) | 850 | 0.8% | 5.0% | 向量化有效,但仍是一次全帧遍历 |
| RGBA→BGRA 通道交换(标量 C) | 320 | 4.2% | 18.0% | 没有 NEON = 格式转换吃掉近 1/5 CPU |
| DMA-BUF mmap 直接写入 vs memcpy 间接写入 | 3× 差距 | — | — | 零拷贝的差距不在吞吐在延迟——省掉 21ms 拷贝等待 |
三个工程结论
第一,像素格式的选择比你想的重。 选 RGB565 省 4.2MB 的代价是失去 Alpha 通道——半透明叠加、边缘抗锯齿、报警图标下方的暗色遮罩全部无法实现。如果你最终还是要 Alpha,那 RGB565 省下的 4.2MB 等于白省——因为你还是要走回 RGBA8888。 反过来,如果你确定不需要 Alpha(纯视频预览、不需要叠加 OSD 浮层),RGB565 + 15Hz 就是正确的选择——每少一个字节就少一份带宽。
第二,Stride 不是"调一调性能"。 Stride 不对齐是指令层面的伤害——每一行像素的地址都不在 cache line 边界上,cache miss 翻倍。一条 ARMv7 ldmia 指令能在一个 cache line 内完成和不能,在性能层面差了一倍。对齐不是优化,是正确性。
第三,DMA-BUF 零拷贝的本质不是"更快",是更少。 少三次 memcpy = 少 21ms 延迟 = 多出来的这些毫秒,你可以多跑一个检测通道、多画一层热力图、多处理一个报警图标。在只有 256MB DDR3 的设备上,"更快 10%" 没那么重要——多跑一个检测通道,才重要。
perf top 热点分布(新增基准)
1. alpha_blend_row() 42% ← 混合仍是第一热点(01 篇同) 2. RGBA→BGRA 通道交换 18% ← 新增:格式转换开销(标量 C,无 NEON) 3. memcpy / __aeabi_memcpy 12% ← 块拷贝(非 DMA-BUF 路径) 4. draw_rect_fill() 10% ← 矩形填充 5. memset 8% ← Buffer 清空 6. 其余 10%
对比 01 篇的 perf 热点:01 篇中 memcpy 占 28%。这里降到了 12%——原因是引入了 DMA-BUF 路径,拷贝次数减少。但消失的 16 个百分点没有"消失"——它被 RGBA→BGRA 格式转换吃掉了。优化从来不是消灭开销,是把开销从一项挪到另一项,然后比较哪一项更值得。
✅ 可复现验证:用
perf record -e cache-misses -g ./render_test && perf report --stdio对比 stride=7680 和 stride 故意偏移 +1 字节场景下的 cache miss 增量。写意留白,是艺术的呼吸;管线确定,是工程的底线。02 / 05 |下期预告:《墨分五色——图层叠加防晕染与 Alpha 混合的确定性边界》