SDL 是一个「把窗口、渲染、声音、事件包成一层」的 C 库。 它的配置问题几乎都能用三行测出来:版本、子系统初始化、视频驱动。
前五章一直在 CPU 这一侧:解码、转格式、滤波、显示。这一章换到另一侧——把像素交给显卡去画。 SDL 就是最薄的那一层胶水,FFplay 整个播放器就是建在它上面的, 第八、九章会一路走到那里。
一、它是一个 C 库,不是一个框架
先把定位说清:SDL 不做解码、不做解封装、不做同步。 它只管四件事:开窗口、往窗口上画、出声、收输入。
| SDL 管 | SDL 不管 |
|---|---|
| 窗口与显示 | 解码视频 |
| 2D 渲染与纹理 | 解封装容器 |
| 音频输出设备 | 音视频同步 |
| 键盘鼠标手柄事件 | 格式探测 |
这个分工决定了它的用法:你自己解码,把解出来的像素塞给它,它负责显示。 所以它跟 FFmpeg 是天然的一对——一个管「拿到像素」,一个管「把像素画出去」。
SDL 这个名字是「Simple DirectMedia Layer」的缩写,重点在 Direct:
它跨平台的方式是「每个平台写一套后端」,而不是「自己模拟一遍」。
所以同一份代码在 Windows 上走 Direct3D、在 Linux 上走 OpenGL,性能没有额外损失。
二、三行自检:版本、初始化、驱动
配 SDL 出问题时,先别改代码,先跑这三行:
查编译期与运行期版本、查 SDL_Init 返回值、查当前视频驱动。
SDL_version v;
SDL_GetVersion(&v);
printf("%d.%d.%d\n", v.major, v.minor, v.patch);
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
printf("init 失败: %s\n", SDL_GetError());
}
printf("驱动: %s\n", SDL_GetCurrentVideoDriver());
第一行那个「版本」有两个来源:编译期版本来自头文件(SDL_MAJOR_VERSION 这一组宏),
运行期版本来自 dll(SDL_GetVersion)。
Demo 里这两个数不一样:编译期 2.30.10、运行期 2.32.10。 这不是错误,而且运行期高于编译期是允许的——反过来才是麻烦。06.05 专门讲这条。
SDL_Init 返回 0 表示成功、返回负数表示失败,这不是「真/假」的习惯,
所以写成 if (SDL_Init(...)) 就是把成功当成失败。
这类错在代码里几乎是静默的:它跳进了错误分支,而错误分支里往往什么都没做。
三、子系统是位掩码,不是枚举
SDL_Init 要的是一个位掩码,要哪几个子系统就把对应的位或起来:
| 子系统 | 常量 | 值 |
|---|---|---|
| 定时器 | SDL_INIT_TIMER |
1 |
| 音频 | SDL_INIT_AUDIO |
16 |
| 视频 | SDL_INIT_VIDEO |
32 |
| 事件 | SDL_INIT_EVENTS |
16384 |
| 手柄 | SDL_INIT_GAMECONTROLLER |
8192 |
这些值都是 2 的幂,所以能按位或:SDL_INIT_VIDEO | SDL_INIT_AUDIO 等于 48。
这种设计的取舍很典型:好处是「一次调用初始化任意组合」,
代价是失败时你不知道是哪一位出的问题——只能靠 SDL_GetError() 提供的那一句话。
还有一个后果:子系统是「引用计数式」的,
SDL_InitSubSystem 调两次就要 SDL_QuitSubSystem 两次才真的关。
所以在库函数里不要随手 SDL_Quit()——你关掉的是别人也在用的东西。
最后说本机这套环境的一个特殊之处:所有 Demo 都跑在 dummy 视频驱动上。
它什么也不画到屏幕上,但会照常走完「建纹理、上传、渲染」的全过程。
所以「渲染管线」这一段是真跑出来的。
只有最后一步输出被我省掉了。
代价是拿不到硬件渲染器,这一点在 06.04 会正面撞上。
四、动手 Demo:SDL 2 的环境自检,三行就够
这段把三行自检全打出来:两个版本号、初始化返回值、当前驱动,再把子系统常量的真实值列一遍。
最后一行「视频驱动 2 个」解释了后面所有 Demo 为什么能跑——里面那个 dummy 就是无显示器时的替身。
import sdl2, ctypes
v = sdl2.SDL_version()
# 编译期版本藏在宏里
print(sdl2.SDL_MAJOR_VERSION, sdl2.SDL_MINOR_VERSION)
# 运行期版本要问 dll
sdl2.SDL_GetVersion(ctypes.byref(v))
print(v.major, v.minor, v.patch)
print(sdl2.SDL_Init(sdl2.SDL_INIT_VIDEO))
print(sdl2.SDL_GetCurrentVideoDriver())
for i in range(sdl2.SDL_GetNumVideoDrivers()):
print(sdl2.SDL_GetVideoDriver(i))
实跑输出:
--------------------------------------------------------------------
SDL 2 的环境自检,三行就够
--------------------------------------------------------------------
**SDL 的配置问题几乎都能用三行测出来**:**版本、子系统初始化、视频驱动。**
**编译期头文件版本**:2.30.10
**运行期 dll 版本**:2.32.10
**这两个数不一样,是本节最该记住的现象**(这里编译期 2.30.10、运行期 2.32.10)。
**头文件是编 Python 扩展时用的,dll 是你现在加载的那个。**
**06.05 会专门讲这件事的后果。**
`SDL_Init(SDL_INIT_VIDEO)` = **0**(0 就是成功)
当前视频驱动 = **dummy**
**`SDL_Init` 的参数是「位掩码」,不是一个枚举**,**要哪几个就把对应的位或起来**:
子系统 常量 值
-----------------------------------
定时器 `0x0001` 1
音频 `0x0010` 16
视频 `0x0020` 32
手柄 `0x0200` 512
力反馈 `0x1000` 4096
手柄映射 `0x2000` 8192
事件 `0x4000` 16384
传感器 `0x8000` 32768
**这些值都是 2 的幂**,所以能按位或:`SDL_INIT_VIDEO | SDL_INIT_AUDIO` 就是 32 + 16 = 48。
**用「或」的好处是「要什么给什么」**,
**代价是初始化失败时你不知道是哪一位出的问题**——只能看 `SDL_GetError()`。
**本机视频驱动 2 个**:
windows
dummy
**`windows` 是真驱动、`dummy` 是「什么也不画」的空驱动。**
**这一课的所有 Demo 都跑在 dummy 上**,**因为沙箱里不该弹窗**,
**而 dummy 会照常走完「创建纹理、上传、渲染」的全过程**,**只是最后一步不输出到显示器。**
**这件事的意义要讲清**:**它证明了「渲染管线」和「显示输出」是两段**,
**前一段在无显示器时也能测。**
**代价是「走 GPU 那一侧」测不到**——dummy 只会给软件渲染器。**这一点 06.04 会正面撞上。**
两个版本号差了两个小版本,这在真实项目里非常常见:
头文件是装 SDK 时带的,dll 是运行时从 PATH 里找到的,两边可能根本不是同一个包。
「视频驱动 2 个:windows、dummy」这行要这么读:
windows 是真正的后端(内部走 Direct3D / GDI),dummy 是「假装成功」的空后端。
dummy 存在的意义不是为了测试:它是给「没有显示器的机器」用的——服务器、CI、容器。
没有它,SDL_Init(VIDEO) 会直接失败,连带整个程序起不来,哪怕这个程序只是要读一下文件。
子系统那张表里有几个值值得记:视频是 32、事件是 16384。
16384 这个数看着奇怪,其实只是第 15 位——1 << 14。
所有子系统常量都是「1 左移若干位」,所以按位或永远不会撞车。
30 秒实验
- 把
SDL_Init(SDL_INIT_VIDEO)改成 0(不初始化任何子系统),看后面建窗口报什么错。 - 算一下:
SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_EVENTS等于多少? - 判断一下:一个库函数里能不能随手调
SDL_Quit()?为什么?
常见误区
- 把
SDL_Init当布尔用。 它返回 0 表示成功、负值表示失败, 写成if (SDL_Init(...))就把成功当失败。 - 以为
SDL_Init的参数是枚举。 它是位掩码,要几个就按位或,一次只能传一个的话就白设计了。 - 在库函数里调
SDL_Quit()。 它会把整个 SDL 关掉,包括别人还在用的子系统。 要用SDL_QuitSubSystem。 - 只看编译期版本就下结论。 运行期 dll 可能是另一个版本,两个都要打出来比较。
自测三问(点开对答案)
1. `SDL_Init` 返回什么表示成功? —— 0。负值表示失败。这是 SDL 全家的约定(返回 0 成功、负值失败),跟「真/假」的布尔约定相反。 2. 为什么子系统常量都是 2 的幂? —— 因为参数是位掩码,靠按位或组合。只有每个常量独占一位,才不会互相覆盖。 3. `dummy` 视频驱动有什么用? —— 在没有显示器的环境(服务器、CI、容器)里让 `SDL_Init(VIDEO)` 成功, 走完渲染管线但不真正输出。环境通了,下一节看 SDL 里到底有哪几个对象。一共四个,而且「哪个走 GPU」这件事经常被搞混。
- ☐ (本节完,下一节 06.02 SDL 2的核心对象)