SDL 里只有四个对象:Window、Renderer、Texture、Surface。 前三个走系统与驱动,只有 Surface 是纯内存。
上一节的 SDL_Init 只是把子系统打开,还什么都没建。这一节建对象,而且要建对顺序。
顺序是死的:Window → Renderer → Texture。
搞错顺序或者漏一个,后面的调用全是空指针。
一、四件套,各自负责什么
| 对象 | 是什么 | 内存在哪 | 能不能直接改像素 |
|---|---|---|---|
SDL_Window |
一个窗口句柄 | 系统 | 不能 |
SDL_Renderer |
画图的上下文 | 系统 + 驱动 | 不能 |
SDL_Texture |
可以直接画的图 | 驱动侧(可能在显存) | 只能整体上传 |
SDL_Surface |
CPU 里的一块像素 | 你进程的堆 | 能,加锁后直接写 |
这张表最重要的一列是最后一列。
只有 Surface 能让你「拿到一个指针、逐像素改」;
Texture 只能整体上传,改一个像素也要重新传一整帧。
所以典型流水线是这样分工的:
Surface 用来「造图 / 改图」(读 PNG、画文字、做特效)。
Texture 用来「显示」(每帧一次上传,然后 RenderCopy)。
两者之间是 SDL_CreateTextureFromSurface(一次转换)。
为什么要分两个对象:因为「能随时改」和「能快速画」是矛盾的。
CPU 内存里改像素很方便,但画的时候要一趟趟搬;驱动侧的内存画起来快,但你碰不到。
这跟 03.02 讲的 QImage 与 QPixmap 是完全相同的分工,只是换了个名字。
二、顺序是死的,而且不能省
三步,一步都不能跳:
SDL_Window *win = SDL_CreateWindow("标题",
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
640, 480, SDL_WINDOW_SHOWN);
SDL_Renderer *ren = SDL_CreateRenderer(win, -1,
SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
SDL_Texture *tex = SDL_CreateTexture(ren,
SDL_PIXELFORMAT_IYUV,
SDL_TEXTUREACCESS_STREAMING, 640, 480);
SDL_CreateRenderer 的第二个参数 -1 是「让 SDL 挑驱动」,第三个参数才是重点:
SDL_RENDERER_ACCELERATED:要硬件加速。
给不了会失败,这正是你想要的——否则你会以为自己在用 GPU,其实一直在用 CPU。
SDL_RENDERER_SOFTWARE:明确要软件渲染。
SDL_RENDERER_PRESENTVSYNC:垂直同步,防止撕裂,也顺便把帧率钉在刷新率上。
这里有个反直觉的地方:ACCELERATED 是「要求」不是「偏好」。
如果机器给不了硬件渲染器,它直接返回空指针,不会退回软件。
想要「有就用、没有就算了」,得写成「先试 ACCELERATED,失败了再建一次 SOFTWARE」。
Demo 里那句渲染器名字是 software,就是因为这段代码明确要了软件渲染器——dummy 驱动下没有硬件渲染器,要 ACCELERATED 会直接失败。
三、创建贵、填充便宜
Demo 建了 100 个 surface 和 100 个 texture 对比:texture 单个要贵两三倍。
原因很简单:surface 只是一次 malloc,texture 要向驱动申请资源,
背后可能是显存分配 + 格式协商 + 内部对象创建。
这个差价的工程含义非常具体:播放器绝对不能「每帧新建纹理」。 640×480 一秒六十帧就是六百次申请, 按每次半个毫秒算,光这一项就是三百毫秒——整个一秒的预算全没了。
正确做法是「建一次,每帧只 SDL_UpdateTexture」:它只搬像素,不动资源。
这条规律在整个图形编程里都成立——
贵的是「创建」,便宜的是「填充」。
还有一个细节值得知道:
SDL_TEXTUREACCESS_STREAMING 是专门给这种情况的——它告诉驱动「这张纹理会频繁改内容」,
驱动因此会选一种「写入快、读取慢」的布局。
反过来 TARGET 是给「渲染到纹理」用的,STATIC 是给「一次上传再不改」用的。
选错不会报错,只会慢。
四、动手 Demo:四件套,谁分配内存、谁走 GPU
这段先查渲染器的名字,再建一个 Surface 看它的 pitch,
最后建 100 个 surface 和 100 个 texture 比成本。
pitch 那个数跟 05.03 的 stride 是同一个东西——换了个库、换了个名字,本质没变。
w = sdl2.SDL_CreateWindow(b"t", 10, 10, 640, 480,
sdl2.SDL_WINDOW_HIDDEN)
r = sdl2.SDL_CreateRenderer(w, -1, sdl2.SDL_RENDERER_SOFTWARE)
# 四件套里只有 surface 能直接摸到像素
s = sdl2.SDL_CreateRGBSurfaceWithFormat(
0, 640, 480, 24, sdl2.SDL_PIXELFORMAT_BGR24)
print(s.contents.pitch, s.contents.pixels)
# 建 100 个比一比
t0 = time.perf_counter()
ss = [new_surface() for _ in range(100)]
print((time.perf_counter()-t0)*1000)
t0 = time.perf_counter()
ts = [new_texture(r) for _ in range(100)]
print((time.perf_counter()-t0)*1000)
实跑输出:
--------------------------------------------------------------------
四件套,谁分配内存、谁走 GPU
--------------------------------------------------------------------
**这台机器上建出来的渲染器叫 `software`**(软件渲染器)。
对象 是什么 内存在哪 谁负责
---------------------------------------------------------------------------------
`SDL_Window` 窗口句柄 系统 你建、你销毁
`SDL_Renderer` 画图的上下文 系统 + 驱动 你建、你销毁
`SDL_Texture` **可以直接画的图** **显存/驱动侧** 你建、你销毁
`SDL_Surface` CPU 里的一块像素 **你自己进程的堆** 你建、你释放
`SDL_Surface` 的 `pitch` = **1920 字节**(宽 640 × 3)
可以 `SDL_LockSurface` 直接读写的指针 = **有**
**`pitch` 这个词跟 MFC 的 `stride` 是同一个东西**:**一行占多少字节。**
**640×3 = 1920 正好被 4 整除,所以这里是 1920、没有填充。**
**换成 641 宽就会出现填充**——**05.03 算过的那笔账,在 SDL 里换个名字又出现一次。**
**建 100 个,看成本的量级**:
100 个 surface:**17.85 毫秒**(每个 0.178 毫秒)
100 个 texture:**50.18 毫秒**(每个 0.502 毫秒)
倍数:**2.8 倍**
**texture 贵两三倍,因为它要向驱动申请资源。**
**这解释了为什么播放器要「复用纹理」而不是「每帧新建」。**
**每帧新建一个 640×480 的纹理,一秒六十次就是六百次申请**,**光这一项就能吃掉几个毫秒。**
**正确的做法是「建一次、每帧只更新内容」**:`SDL_UpdateTexture` 只搬像素,不动资源。
**这条规律在 GPU 编程里到处都成立**:**贵的是「创建」,便宜的是「填充」。**
渲染器名字那一行是本节最该看的一个数:software 说明走的是 CPU。
真实机器上写 ACCELERATED,这里应该出现 direct3d 之类的名字。
记住这个自检手法:问渲染器名字,比猜有用得多。
pitch = 1920 那一行跟 05.03 的 stride 表完全对得上:640×3 = 1920,正好 4 字节对齐,
所以没有填充。换成 641 宽,pitch 就会变成 1924。
「可以加锁读写的指针 = 有」这句话也别跳过:surface 能给你 pixels 指针,
texture 给不了——它连「指针」这个概念都没有,只有一个「上传接口」。
这就是「能不能改像素」这条分界线的物理形态。
建 100 个那三行数据的绝对值不重要,倍数才重要:两到三倍。 这个倍数在真实机器上会更大——因为真显存的分配比软件渲染器的模拟要贵得多。 所以「复用纹理」这条经验在真机上比在这里更值钱。
30 秒实验
- 把渲染器参数从
SOFTWARE改成ACCELERATED,看能不能建出来。 - 算一下:640×480 的
BGR24surface,pitch是多少?换成 641 宽呢? - 判断一下:要把一张 PNG 每隔几秒换一次,该用 surface 还是 texture?
常见误区
- 先建纹理再建渲染器。 纹理必须挂在渲染器上,顺序反了就是空指针。
- 以为
ACCELERATED是「尽量」。 它是硬要求,给不了硬件渲染器就返回空。 要有回退就自己写两次尝试。 - 每帧
SDL_CreateTexture。 一次创建比一次填充贵两三倍,一秒六十次会把预算吃光。 要建一次、反复Update。 - 拿 texture 当 surface 用,想直接改一个像素。 texture 没有可写指针,改一个像素也要整帧重传。
自测三问(点开对答案)
1. SDL 的四个核心对象是什么? —— `SDL_Window`(窗口)、`SDL_Renderer`(渲染上下文)、`SDL_Texture`(驱动侧的图)、 `SDL_Surface`(CPU 侧的像素)。 2. 为什么只有 Surface 能逐像素改? —— 因为它的像素在你自己进程的堆里,加锁就拿到指针。 Texture 的内容在驱动侧,接口只提供「整体上传」。 3. `SDL_TEXTUREACCESS_STREAMING` 是什么意思? —— 告诉驱动这张纹理会频繁更新内容,驱动会选写入更快的布局。选错不会报错,只会慢。四个对象会用了,下一节补外围。SDL 本体不会读 PNG、不会排文字、不会解码 MP3——这些都要另装库,而且每个都要单独初始化。
- ☐ (本节完,下一节 06.03 SDL 2的扩展库及应用)