在浏览器里跑老版《雷神之锤》,本身早已不是什么新鲜事。过去十多年间,借助 Emscripten 交叉编译原版 C 代码,或者利用 WebGL 包装一层现代着色器管线,社区已经做过无数次复刻。

开源移植项目 Quake-SRP 的反常之处在于,它彻底扔掉了现成的编译拐杖。开发者 terrapapagalli1516 拿 id Software 1996 年开源的 WinQuake GPL 源码动刀,将其完整重构成 Rust 版本。整个代码库在编译阶段直接启用了禁止任何 unsafe 代码的约束,除了 Rust 标准库外没有任何外部依赖,并且全程依赖 CPU 软件渲染,直接在浏览器网页端免安装运行。

剥离 C 工具链,把光栅化压进 Web Worker

在浏览器的沙箱环境里运行重度依赖桌面系统底层特性的老游戏,传统路径几乎被 Emscripten 垄断。这类方案本质上仍是 C 语言工程,保留了大量指针跳转与未经检查的内存访问。

复杂的光栅化计算被彻底隔绝在独立的后台工作线程中(示意图)
复杂的光栅化计算被彻底隔绝在独立的后台工作线程中(示意图)

Quake-SRP 换了一条极其极客的实现路径。它将编译目标设定为 wasm32-wasip1-threads,把 WASI 编译产物塞进独立的 Web Worker 线程池运行,宿主端仅保留一套轻量的 JavaScript WASI 胶水代码。这种设计将计算线程与主线程彻底隔离,保证了游戏主循环和音频混音不会冻结页面交互。

Code
浏览器主线程 (UI / 输入捕获) <---> Web Worker (WASI 多线程宿主)
                                        │
                                        ▼
                                 Safe Rust 游戏循环
                                        │
                                 CPU 软件光栅化渲染
                                        │
                                 生成索引颜色帧缓冲区
                                        │
                                        ▼
                                WebGL2 (仅作最终像素点对点搬运)

更决绝的是图形管线的设计。项目没有调用现代图形 API 跑 3D 渲染,而是忠实复现了 1996 年原版的纯 CPU 软件渲染器。在整个技术栈中,WebGL2 并没有参与任何场景多边形变换或光影计算,它的唯一作用仅仅是充当一个输出通道,把 CPU 在内存中算好的索引颜色帧缓冲区贴到屏幕上。

Quake-SRP 纯 CPU 软件渲染数据管线 轻量 JS 宿主 Web Worker 键盘与鼠标捕获 WASI 线程调度 Safe Rust 渲染核 BSP 树空间分割遍历 无裸指针的像素光栅化 forbid(unsafe_code) 约束 屏幕像素呈现 8位索引色帧缓存 WebGL2 仅作最终搬运 无 GPU 着色运算 全管线零外部依赖,纯 CPU 单像素填充由 Rust 所有权模型受控管理

该页面内置了 9.1MB 的 shareware 原版 quake106.zip 数据,同时支持玩家直接把正式版数据包 pak1.pak、资料片以及 CD 音轨拖进浏览器。项目目前仅支持单人游戏,作者在开发说明中记录了其在渲染画面、音频混合以及内置 demo 状态机上,与 1996 年原版 C 语言引擎逐帧进行的像素级对齐验证。

从指针泥潭到零 unsafe 的架构洁癖

把图形引擎从 C 搬到 Rust,行业里此前经历过一次巨大的心理落差。

杂乱跳跃的裸指针被全数拆除,规整纳入严格受控的线槽约束(示意图)
杂乱跳跃的裸指针被全数拆除,规整纳入严格受控的线槽约束(示意图)

2020 年前后,社区曾尝试使用自动化转换工具 c2rust 将《雷神之锤 3》转译为 Rust。转译的结果是一个塞满了原始裸指针和无处不在的 unsafe 标记的代码仓库。那次尝试引发了广泛的质疑:如果只是把 C 代码语法机械翻译成套着 Rust 壳的 unsafe 语法糖,重构的收益到底在哪里?

机械转译确实暴露出原版 C 引擎深藏的逻辑越界缺陷,例如在实体推挤检测函数 G_TryPushingEntity 中,原作者将边界比较操作符混淆导致的越界解引用,在 Rust 严格的编译类型约束下一览无余。但它留下的残局依然脆弱,任何一次解引用失误依然能瞬间让整个进程崩溃。

C 代码转写 Rust 的两种工程范式 2020: 自动化 c2rust 转译 • 保留大量裸指针解引用 • 充斥 unsafe 块,防御力极低 本质:套着 Rust 语法的 C 运行时 当下: Quake-SRP 原生重构 • 全局 forbid(unsafe_code) • 标准库所有权取代全局内存池 本质:完全受控的内存安全模型 从机械迁移走向架构重写,安全语义不再依靠人工注释维系

Quake-SRP 走向了另一个极端。它全库启用了宏指令禁止 unsafe 代码,并舍弃所有第三方依赖包。

真正的现代化重构不是替指针换个马甲,而是敢在编译器规则内重建秩序。

古人讲不以规矩,不能成方圆。在图形编程里,规矩往往意味着束缚。原版 WinQuake 之所以快,靠的是约翰·卡马克极其激进的底层 hack:裸指针在内存池里直接跳跃寻址、全局状态随意共享、多边形扫描线填充直接向显存地址灌数据。Quake-SRP 必须在 Rust 借用检查器的铁律下重写这些逻辑。原本一个裸指针就能解决的多边形表面缓存访问,现在必须被拆解成合法切片、生命周期标注和严格的所有权流转。


边界检查的隐形税与工程盲区

这种工程洁癖在技术上令人振奋,但在工程落地上并非毫无代价。

装有微型游标卡尺限位闸门的流水线导轨:沿金属导轨匀速向前滑移的一整列方形像素工件方块(示意图)
装有微型游标卡尺限位闸门的流水线导轨:沿金属导轨匀速向前滑移的一整列方形像素工件方块(示意图)

第一个摆在面前的成本是运行时边界检查。原版 C 引擎在遍历光栅化像素跨度时,追求的是unchecked的极致内存写入速度。而在没有 unsafe 绕道特权的 Safe Rust 中,每一次数组索引访问都默认背负着越界安全检查的微小开销。即使 LLVM 优化器能消除部分循环内的检查分支,在复杂的嵌套多边形光栅化算法下,这种防御性设计依然会构成不可忽视的性能税率。

第二个无法回避的现实是审慎性验证。目前零 unsafe 与零第三方依赖的标签,仍属于作者自我声明的技术特征,尚未经历过严格的独立第三方安全审计。在缺乏大规模工业级基准测试的情况下,这套纯 CPU 软件渲染器在低配设备或高分辨率高负载场景下的实际帧率表现,仍需打上一个审慎的问号。

  • 风险.纯 CPU 软件渲染避开了 GPU 加速红利,若在移动端或算力受限设备上运行,密集的数组边界检查可能引发帧率波动。

这也解释了为什么项目目前止步于单人模式。复杂的网络预测算法和客户端-服务端通信逻辑,一旦同样置于严格的纯安全约束与 WASI 线程沙箱中,系统状态同步与动态内存管理的难度会呈几何级数增加。

  • 建议.关注该架构后续是否会扩展至多人网络对战模块,那才是检验 Safe Rust 能否通吃复杂游戏状态机的真正试金石。

Quake-SRP 的真正价值,并不在于它能提供多么丝滑的游玩体验,而在于它设立了一个极高的工程坐标。它证明了即使在传统认知中极度依赖指针杂技的底层多媒体与图形学领域,Safe Rust 同样具备完整的表达能力。那种高性能必须向内存漏洞妥协的行业共识,在这台被完全规训的 1996 年古董引擎面前,撕开了一道明显的裂痕。