墨趣Vivid 02|宣纸的质地——Framebuffer、像素格式与内存带宽的工程权衡

23 阅读22分钟

画家选纸,指腹一搓就知道纤维粗细。工程师选 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精度 × 字节数 × 显示控制器兼容性这五个变量的约束方程。选格式不是挑纸的"手感",是解一道由五维约束构成的工程方程。

格式矩阵:每一种都有代价

格式BPP1080p 单帧Alpha 精度适用场景
RGBA888848.3 MB256 级工业 HMI,合规对比度
BGRA888848.3 MB256 级DRM/KMS 常见格式(受 SoC 字节序约束)
ARGB888848.3 MB256 级部分 ARM Mali IP 偏好
RGB56524.1 MB预览流,非合规叠加
ARGB155524.1 MB1 位(二值透明)简单掩膜,无半透
A8(灰度)12.1 MB256 级(仅遮罩)单通道遮罩层
NV12(YUV semi-planar)1.53.1 MB摄像头采集原生格式
I420(YUV planar)1.53.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_BGRA8888DRM_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 atomic64 字节ALIGN(7680,64) = 7680 ✅恰好整除
i.MX6ULL (PXP)FBDEV32 字节ALIGN(7680,32) = 7680 ✅恰好整除
Allwinner V3sDE-BE16 字节ALIGN(7680,16) = 7680 ✅恰好整除
老式 ARM9 屏驱(ILI9xxx)LCD 控制器1024 字节ALIGN(7680,1024) = 8192 ⚠️多了 512
x86 (i915)DRM modern64-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(canvas0width * height * bpp); // ✅ 正确:实际步长清零——覆盖全部 buffer,包括对齐填充 memset(canvas0height * 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 

代入几个典型配置:

分辨率格式帧率显示带宽备注
720pRGB56530 Hz55 MB/s最省
720pRGBA888830 Hz110 MB/s
1080pRGB56530 Hz124 MB/s
1080pRGBA888830 Hz249 MB/s01 篇基准
1080pRGBA888860 Hz498 MB/s
4KRGBA888830 Hz996 MB/sDDR3 扛不住

注意——这个 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/sYUV 422, 1920×1080@30fps
GUI 软件混合读写~130 MB/s01 篇 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, 000 }; uint32_t offsets[4] = { 0000 }; drmModeAddFB2(drm_fd, 19201080, 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/scache-miss%CPU%关键发现
memcpy 整帧(Stride 7680, 64B 对齐)12000.3%2.1%基线:对齐 = 高效
memcpy 整帧(Stride 7936, 64B 对齐但多填充)11800.4%2.3%填充字节不参与 memcpy,开销来自多循环次数
RGBA→BGRA 通道交换(NEON vld4+vswp+vst4)8500.8%5.0%向量化有效,但仍是一次全帧遍历
RGBA→BGRA 通道交换(标量 C)3204.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 混合的确定性边界》