SDL_UpdateYUVTexture 一次收三个平面,不做任何转换。
它的好处在硬件渲染器上才兑现;软件渲染器上,转换只是搬了个地方。
上一节把外围补齐了,这一节回到核心:解码器给你的是 YUV,SDL 纹理能不能直接吃。
能。这就是 SDL_UpdateYUVTexture 存在的理由——而它的意义要靠「少一次转换」和「少一半带宽」两笔账算出来。
一、三个平面,各有各的跨度
YUV 420 的「420」是什么意思:Y 是全分辨率,U 与 V 各是长宽都减半, 所以总字节数是 Y 的 1.5 倍。
| 平面 | 尺寸(640×480) | 字节数 | 含义 |
|---|---|---|---|
| Y | 640×480 | 307200 | 亮度,全分辨率 |
| U | 320×240 | 76800 | 蓝色色度,各方向减半 |
| V | 320×240 | 76800 | 红色色度,各方向减半 |
三个平面加起来 460800 字节,同样大小的 RGB24 要 921600 字节。 正好一半——这就是「YUV 省带宽」这句话的全部算术。
接口要六个参数,其中三个是「行跨度」:
SDL_UpdateYUVTexture(tex, NULL,
y_ptr, y_pitch, // 平面 0
u_ptr, u_pitch, // 平面 1
v_ptr, v_pitch); // 平面 2
跨度别传成宽度,这是本节最容易踩的坑。 解码器给的行跨度经常大于宽度——因为 SIMD 指令喜欢对齐的内存,解码器会在每行末尾留填充。 按宽度传的结果就是斜条纹,跟 05.03 在 MFC 里踩的是同一个坑,换了个库又出现一次。
这次的顺序是反的:05.03 是「Windows 要求 4 字节对齐,你要自己补」, 这里是「解码器已经补好了,你要把跨度告诉接口」。 同样是填充,一边要加、一边要用。 唯一不变的是「填充永远存在」。
二、两种写法:整帧上传,还是三平面
| 写法 | 前提 | 省了什么 |
|---|---|---|
喂 RGB,再 UpdateTexture |
先 sws_scale 转一次 |
什么都不省 |
| 喂 YUV 三平面 | 解码出来就是 yuv420p | 省掉整次色彩转换 |
第一行的代价要算清:sws_scale 是逐像素的活,1080p 上一次要几毫秒,
60 fps 下就是几十个百分点的核。
而这一步在「喂 YUV」那条路上是零。
但 Demo 结果里有个意外:YUV 那条路快,主要不是因为「上传快」,而是因为「省了 reformat」。 上传那一段两者只差一点点。
为什么上传没快一半:因为渲染器是软件渲染器。 它拿到 YUV 之后要自己用 CPU 转成 RGB 才画得出来, 于是「转换」这件事并没有消失,只是从你的代码搬到了渲染器里。
这就是本课最想让人记住的那种「测不出来」: 结论的方向是对的(YUV 更快),但其中一半的收益在沙箱里被吃掉了。 真实情况要在有硬件渲染器的机器上才算得出来, 这里只能把「已测到的那一半」讲透,把「测不到的那一半」说清为什么。
三、为什么硬件那边才划算
GPU 做 YUV 转 RGB 是「免费」的:纹理采样器里有一个固定的色彩矩阵, 采样时顺手就乘完了,不占额外的计算单元。
所以硬件渲染器下,两条路的差别是这样的: YUV:上传 460800 字节,GPU 采样时顺手转换,零额外成本。 RGB:CPU 转一次(几毫秒),上传 921600 字节(两倍带宽),GPU 直接采样。
两笔账都输:多一次 CPU 计算、多一倍上传带宽。 1080p 60 fps 下,光带宽就是每秒 110 MB 对 220 MB。
所以「解码出来是什么就喂什么」是播放器的基本原则:不要在 CPU 上做任何可以交给 GPU 的转换。 这一条在第七、八章会反复出现——FFplay 从头到尾就没转过格式。
最后提一句硬件渲染器拿不到时该怎么办: 降级到软件渲染器,然后接受「YUV 的优势变小」这个事实。 这时候更好的选择反而是「解码时就缩到小尺寸」,因为软件渲染器下,省像素比省转换更有效。
四、动手 Demo:YUV 三平面直接喂给纹理,一次转换都不做
这段真解码一帧 yuv420p,把三个平面直接传给 SDL_UpdateYUVTexture,
再跟「先 reformat 成 RGB 再上传」对比四条路径的耗时。
表里最后两行要加起来看——RGB 那条路是「转换 + 上传」两步,不能只比上传。
fr = next(c.decode(video=0)) # yuv420p
# 三个平面的跨度要照抄,不能传宽度
sdl2.SDL_UpdateYUVTexture(tex, None,
fr.planes[0].buffer_ptr, fr.planes[0].line_size,
fr.planes[1].buffer_ptr, fr.planes[1].line_size,
fr.planes[2].buffer_ptr, fr.planes[2].line_size)
# 对比路径
fr.reformat(format="rgb24") # 这一步纯 CPU
sdl2.SDL_UpdateTexture(tex2, None, bgr.ctypes.data, 640*3)
实跑输出:
--------------------------------------------------------------------
YUV 三平面直接喂给纹理,一次转换都不做
--------------------------------------------------------------------
**解码出来这一帧是 `yuv420p`、640×480**,正好是 SDL 最喜欢的格式。
**三个平面各自的「行跨度」要看清**:
平面 宽 高 行跨度 为什么
------------------------------------------------
Y 640 480 640 等于宽度
U 320 240 320 等于宽度
V 320 240 320 等于宽度
**U 与 V 是 Y 的四分之一**——这就是 YUV 420 里那个 420 的意思。
**所以一帧只占 307200 + 76800 + 76800 = 460800 字节**,**而同样大小的 RGB 要 921600 字节。**
**「行跨度」那一列要传到接口里,不能传宽度。**
**这段测试片恰好等于宽度,换一段就可能多出几十字节的填充。**
**按宽度传就是斜条纹**,跟 05.03 那个 stride 错位一模一样。
`SDL_UpdateYUVTexture` 返回 **0**(0 就是成功)
**两条路各跑 200 帧,比一比**:
路径 要不要转换 一帧耗时 说明
-----------------------------------------------------------------------
YUV:上传 + 渲染 不转 **0.545 毫秒** 三平面直接给
只做 `reformat` 要转 **0.346 毫秒** 这一步纯 CPU
RGB:上传 + 渲染 已转过 **0.648 毫秒** 还要加上一步
RGB 路线合计 要转 **0.994 毫秒** 两步相加
**把最后两行加起来看,结论就清楚了**:
**YUV 一条 0.545 毫秒,RGB 整条要 0.994 毫秒,差了 1.8 倍。**
**省下来的那一大块不是上传,是那次 `reformat`。**
**上传那一段两者只差 0.103 毫秒**(0.545 对 0.648)。
**跟「少传一半字节」的预期相比,这个小得有点意外。**
**意外的原因在渲染器上**:dummy 驱动只给**软件渲染器**,
**它拿到 YUV 之后要自己用 CPU 转成 RGB 才画得出来。**
**也就是说:转换没有被省掉,只是从你的代码里搬到了渲染器里。**
**所以在软件渲染器上,YUV 只赢在「少一次转换」**,**赢不到「上传更快」那一份。**
**那一份要在硬件渲染器上才兑现**——那边是 GPU 用固定的色彩矩阵一步做完,几乎不要时间。
**这一侧的对比,沙箱里测不到,只能靠规格说话。**
**不过有一条是确定的,跟硬件无关**:**上传的字节数少了一半。**YUV 420 是每像素 1.5 字节,
RGB 是 3 字节,**640×480 差 460800 字节,1080p 差 3110400 字节。**
**如果数据要过总线(独显就是这样),这笔带宽是实打实省下来的。**
表里四个数要按「两步」去读: YUV 一条只要 0.5 毫秒左右,RGB 那条要「转换 0.3 多 + 上传 0.6 多」将近一秒的十分之一。 所以整体差了将近两倍。
但「上传那一段只差 0.1 毫秒」这个数才是本节最值钱的观察。 按「少传一半字节」的预期,这里应该差将近一半——没有,因为软件渲染器把 YUV 转 RGB 的活接过去了。
换句话说:软件渲染器上用 YUV,只是把转换挪了个位置。
你省了 reformat 的调用,但渲染器内部替你做了同一件事。
真正「不做转换」的只有硬件渲染器。
平面跨度那一列全是「等于宽度」,这是测试片太规整的结果。
换一段从摄像头来的、或者分辨率不是 32 倍数的片子,跨度就会大于宽度——那时候按宽度传就是斜条纹。
所以「照抄 line_size」这条规矩,不是因为这次对上了就可以省。
最后那句「1080p 差 3110400 字节」是按每帧算的:乘 60 就是每秒 186 MB 对 93 MB。 这个量级已经接近一条 PCIe 通道的实用带宽,所以「少一半」在真机上不是小数点后的差别。
30 秒实验
- 把
line_size换成宽度重跑,看画面(或者RenderCopy的结果)有没有变斜。 - 算一下:1080p 的 yuv420p 一帧多少字节?60 fps 下每秒多少 MB?
- 判断一下:软件渲染器下,是「喂 YUV」划算还是「解码时缩到 640×480 再喂 RGB」划算?
常见误区
- 跨度传成宽度。 解码器的行跨度经常大于宽度,按宽度传就是斜条纹。照抄
line_size/pitch。 - 以为喂 YUV 就一定快。 软件渲染器会自己转一遍,优势只剩「省了一次 reformat」。 要硬件渲染器才全额兑现。
- 在 CPU 上先
sws_scale转 RGB 再上传。 多一次逐像素计算,多一倍上传带宽,两笔账都输。 - 只比「上传」那一段就下结论。 RGB 那条路是「转换 + 上传」两步,要加起来比才公平。
自测三问(点开对答案)
1. YUV 420 的三个平面各多大? —— Y 是全分辨率,U 与 V 长宽各减半。640×480 时是 307200 + 76800 + 76800,总计 1.5 倍 Y 的大小。 2. `SDL_UpdateYUVTexture` 的三个跨度为什么不能传宽度? —— 因为解码器为了内存对齐会在每行末尾留填充,行跨度大于宽度。 按宽度传就会逐行错位,画面变斜条纹。 3. 为什么在软件渲染器上喂 YUV 省不了多少时间? —— 因为软件渲染器内部要用 CPU 把 YUV 转成 RGB 才能画。 转换没有消失,只是从你的代码搬到了渲染器里。渲染这条线走通了,下一节换个话题:编译。SDL 的编译期问题特别有代表性——版本、位数、运行库,三样必须对齐。
- ☐ (本节完,下一节 06.05 VS 2015编译并运行SDL 2的相关案例)