117 条指令里,42 条在把结果写回表
UPDATE things t
SET x = CASE WHEN s.blocked THEN t.x ELSE t.x + t.mom_x END,
y = CASE WHEN s.blocked THEN t.y ELSE t.y + t.mom_y END,
-- FRICTION once it has moved; a blocked step loses its momentum outright,
-- a skull or an airborne thing keeps it.
mom_x = CASE WHEN s.blocked THEN 0 WHEN s.skull OR s.airborne THEN
t.mom_x ELSE t.mom_x * friction END,
mom_y = CASE WHEN s.blocked THEN 0 WHEN s.skull OR s.airborne THEN
t.mom_y ELSE t.mom_y * friction END
这段 SQL 是 Doom 里物体移动的一步:被墙挡住就原地不动、动量清零,飞行中的骷髅保留动量,其余物体乘上 0.90625 的摩擦系数。1993 年 linux_doom 的 C 原版做同一件事,gcc -O2 编出 48 条 x86 指令;CedarDB 把这段 SQL 降到 LLVM IR 再编成机器码,每行 117 条。
单看比例,SQL 版贵了一倍多。拆开看,117 条里有 42 条在做同一件事:把算好的值写回 things 表,C 版直接改内存里的结构体,没有这一步。刨掉写回,表达移动逻辑本身用了 75 条指令,对 48 条,差距收窄到 1.6 倍左右。
CedarDB 工程师 Lukas Vogel 在 9 月 22 日发布了 SQLDoom:原版 Doom 的游戏逻辑、游戏状态和渲染器全部住在数据库里,实测 35Hz 游戏循环、笔记本上最高 60 FPS,四人联机也跑通了。「数据库能跑 Doom」的猎奇点很容易抢走注意力,这个项目更有分量的产出是一组实测数字。关系模型加编译式查询引擎表达一个实时游戏,代价集中在数据进出表的那一段,逻辑本身便宜得出人意料。
图:同一步物体移动,左侧为 linux_doom 的 C 代码与 gcc 输出,右侧为 SQLDoom 的 SQL 与 CedarDB LLVM 后端输出。注意右侧汇编里反复出现的「into the update record」注释,那就是写回表的开销。来源:CedarDB 博客(We ported the original Doom to SQL)
Python 只管键盘和屏幕,110 张表管其余一切
SQLDoom 的分工很清楚:Python 用 pygame 处理输入、计时和显示,其余全部交给数据库。整库约 110 张表、100 多个函数;游戏逻辑约 5900 行 SQL,原版 C 做同样的事约 9000 行。声明式写法省掉了循环、指针和手工内存管理,行数少了三分之一,与「SQL 表达力有限」的印象正好相反。
数据层的契合度也高。Doom 的 .wad 文件本身就是一套关系结构:顶点(VERTEXES)、线段(LINEDEF)、墙面(SIDEDEF)、扇区(SECTOR)、物体(THINGS)层层引用。约 1000 行 Python 导入脚本,18 秒导完 Doom 1 全部数据,id Software 在 1993 年为 486 设计的格式,三十多年后几乎原样落进了关系表。
作者在写游戏逻辑时撞上了一个顿悟:这套结构就是 ECS(Entity Component System,实体组件系统)。每个组件是一张表,每个系统是一组 UPDATE/INSERT,实体 ID 作为 join key。处理一群敌人不用写 for 循环逐个遍历,一条 UPDATE ... WHERE 交给数据库并行执行,游戏行业花了十几年推广的 ECS,在关系数据库里是默认形态。
46 只怪物冲向一扇门,tic 只用掉 37% 预算
原版 Doom 的游戏逻辑固定 35Hz,每个 tic 预算 28.6 毫秒。作者翻出的最慢一个 tic 在 E4M1:46 只怪物同时冲向一扇正在打开的门,耗时 10.45 毫秒,占预算约 37%。场上 6 只怪物的典型 tic 平均 2.15 毫秒,约 8%,最坏情况下还剩六成余量,逻辑这一侧不构成瓶颈。
图:E4M1 最坏情况下单个 tic 的耗时分解。10.45 毫秒里有 7.02 毫秒花在 line specials(门、开关等线段触发逻辑)上,46 只怪物的视线、追击和攻击合计只有 1.33 毫秒。来源:CedarDB 博客(We ported the original Doom to SQL)
瀑布图里最反直觉的一格是怪物 AI。46 只怪物的视线判断、追击和攻击加起来 1.33 毫秒,因为它们是同一条语句处理的 46 行,数据库按集合一次扫完。吃掉时间的是那扇门触发的线段特殊逻辑,7.02 毫秒,占整个 tic 的三分之二。实体数量涨了,基于集合的写法几乎不涨成本;贵的是牵动关卡几何的少数事件。
渲染器用排序和聚合冒充循环
渲染比游戏逻辑难得多,因为 Doom 渲染器的核心是一连串带可变状态的循环。SQLDoom 的每一帧是一个巨型视图:输入是关卡几何、游戏状态和玩家位置,输出是 64000 行 (x, y, rgb),最后用 string_agg 拼成一行 192,000 字节,正好是 320x200 的帧缓冲。渲染器约 1300 行 SQL(不含注释),摊在 89 个 CTE 里,原版渲染引擎约 3300 行 C。
BSP 遍历是第一个要翻译的命令式算法。Doom 用预烘焙的 BSP 树得到从前到后的绘制顺序,原版靠递归下降;SQLDoom 在加载阶段预计算每条 BSP 路径,把每一步「走前枝记 0、走后枝记 1」写进一个 bigint 的第 40-depth 位,渲染时 SUM 再 ORDER BY,一个聚合就排出了正确的前后顺序。最深的 E4M8 只有 32 层,40 位够用;节点包围盒顺手做视锥剔除,BOOL_AND(keep) 丢掉祖先已被剔除的 subsector。
地板和天花板(visplane)更难。原版用 ceilingclip / floorclip 两个可变数组做洪水填充,SQL 里没有循环也没有可变状态。SQLDoom 改用窗口函数:每个屏幕列上按深度排好 panel,用 ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING 取前面所有 panel 的 MAX/MIN 当作裁剪边界。作者自己管这叫「拿排序和聚合伪装命令式算法的 hack」,它跑得通,代价是每列都要排一次序。
图:单帧渲染各阶段耗时。灰色条是不产生像素的阶段(BSP 遍历、深度解算、调色板转 RGB、合成帧缓冲、排序打包),这一帧里合计约 13.6 毫秒,占总耗时六成以上。来源:CedarDB 博客(We ported the original Doom to SQL)
深度解算是翻译失败的地方。Doom 靠精心编排的绘制顺序省掉 Z-buffer,SQL 里复刻不出来,作者退回暴力法:先生成所有候选像素,把深度(17.12 定点数左移 34 位)、表面优先级、稳定的 tiebreak 和颜色打包进一个 bigint,按像素 GROUP BY 取 MIN。这一步平均 8.2 毫秒,超过整帧三分之一;相比之下墙体绘制平均 1.7 毫秒,地板、天花板和天空约 3 毫秒。
渲染这边的账和开头那 42 条写回指令是同一本账。画像素的阶段加起来只占小头,大头花在深度比较、颜色转换、排序和打包这些搬运数据的环节。作者的 Ryzen 7 PRO 7840U 笔记本上通常约 60 FPS,场景繁忙时掉到 35 FPS,仍然守住了原版的逻辑帧率。
每个 tic 包进一个事务,联机几乎免费
数据库替 SQLDoom 省下的最大一块工作是联机。认证、并发控制、访问控制、一致快照、二进制线协议,这些游戏服务器要自己写的东西,数据库原本就有。每个 tic 包在一个事务里,四名玩家看到的要么是提交前的世界,要么是提交后的世界,「火箭到底打中没有」这类分歧没有出现的空间。
安全模型用的也是数据库现成的权限系统。四个玩家角色只能调用少数几个 API 函数,其余权限全部 revoke;api_input 以 SECURITY DEFINER 执行,前进和侧移输入被 clamp 到 -1 到 1,改客户端发超速指令没有用。资源上,每个客户端 3 个核可以稳定 35 FPS,再加一个核跑 tic 驱动,16 核机器足够撑起 4 人 deathmatch。
「一切皆数据」是另一个副产品。霰弹枪是表里的一行:7 颗弹丸、每颗 3d5 伤害、射程 2048;武器动画的状态机是 12 行数据。改一行就是一个 mod,作者现场把霰弹枪改成一次喷 500 颗弹丸,mod 社区过去靠逆向和十六进制编辑做的事,在这里是一条 UPDATE。
作者说这是坏主意,数据说代价可控
Vogel 自己的结论很克制:「在数据库里渲染 Doom 显然是个坏主意。」他坚持值得捍卫的只有两点,一切皆数据和联机几乎免费。他还指出,SQLDoom 比前作 DOOMQL(2025 年的 ASCII 光线投射版,约 30 FPS,更像 Wolfenstein 3D)更快、更逼真,功劳要算给 Carmack 当年在 486 上压榨出来的 BSP 方案。
把全文数字摆在一起:逻辑代码比 C 少三分之一,最坏 tic 用掉 37% 预算,同一步移动多出的 69 条机器指令里有 42 条是写回表,单帧渲染六成时间花在不画像素的环节。「关系模型表达不了复杂实时逻辑」的说法,在这组数字面前站不住。SQLDoom 测出的成本主要是一笔数据进出关系表的搬运税,这笔税是编译式数据库可以继续压的部分,表达力本身已经够用。
参考链接:
- CedarDB 博客:We ported the original Doom to SQL
- cedardb/sqldoom 仓库
- Hacker News 讨论帖