🎬 第六章 SDL 2开发库及高级应用
📄 一句话概括

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 秒实验

  1. 把 SDL_Init(SDL_INIT_VIDEO) 改成 0(不初始化任何子系统),看后面建窗口报什么错。
  2. 算一下:SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_EVENTS 等于多少?
  3. 判断一下:一个库函数里能不能随手调 SDL_Quit()?为什么?

常见误区

  1. 把 SDL_Init 当布尔用。 它返回 0 表示成功、负值表示失败, 写成 if (SDL_Init(...)) 就把成功当失败。
  2. 以为 SDL_Init 的参数是枚举。 它是位掩码,要几个就按位或,一次只能传一个的话就白设计了。
  3. 在库函数里调 SDL_Quit()。 它会把整个 SDL 关掉,包括别人还在用的子系统。 要用 SDL_QuitSubSystem。
  4. 只看编译期版本就下结论。 运行期 dll 可能是另一个版本,两个都要打出来比较。
自测三问(点开对答案) 1. `SDL_Init` 返回什么表示成功? —— 0。负值表示失败。这是 SDL 全家的约定(返回 0 成功、负值失败),跟「真/假」的布尔约定相反。 2. 为什么子系统常量都是 2 的幂? —— 因为参数是位掩码,靠按位或组合。只有每个常量独占一位,才不会互相覆盖。 3. `dummy` 视频驱动有什么用? —— 在没有显示器的环境(服务器、CI、容器)里让 `SDL_Init(VIDEO)` 成功, 走完渲染管线但不真正输出。

环境通了,下一节看 SDL 里到底有哪几个对象。一共四个,而且「哪个走 GPU」这件事经常被搞混。

相关笔记