引言:复古编译的工程挑战
1999 年 id Software 开源 Quake 引擎时附带的 readme.txt 中,John Carmack 特别指出:"Masm is also required to build the assembly language files. It is possible to change a #define and build with only C code, but the software rendering versions lose almost half its speed." 这句话揭示了一个关键事实 ——Quake 的性能高度依赖 Michael Abrash 手写的手工优化汇编代码。
近三十年后,Fabien Sanglard 在 2026 年完成了一个极具价值的工程考古项目:在 VirtualBox 虚拟机中完整复现 1997 年的 Quake 编译环境。这项工作不仅是对游戏史的致敬,更为现代开发者处理遗留代码兼容性提供了可复现的参考路径。
工具链考古:从 VC++4 到 VC++6 的迁移
Quake 的 Win32 版本最初使用 Visual C++ 4.X 在 Windows NT 上开发,这是 1996 年中期可用的最新 Microsoft IDE。到 1999 年源码发布时,项目已迁移至 Visual C++ 6。现代复现需要严格遵循以下依赖链:
基础安装序列:
- Windows NT 4.0 或 Windows 98SE(支持 SMP 需重装系统以识别双 CPU)
- Visual C++ 6.0 标准版或专业版
- Visual Studio 6.0 Service Pack 5(vc6sp5.exe)
- MDAC 2.5(位于 sp5 解压目录的 mdac_typ.exe)
- VC++6 Processor Pack(vcpp5.exe)—— 提供 ml.exe 汇编器
一个关键的陷阱是工作区文件格式。VC++6 使用.dsw(工作区)和.dsp(项目)文件,而现代 Visual Studio 使用.sln 和.vcxproj。更隐蔽的问题是换行符敏感性 —— 从 GitHub 通过 FTP 获取的源码会破坏.dsw 文件,导致 VC++6 打开后显示 "无关联文件" 且不提供错误提示。必须使用原始 q1source.zip 中的文件,通过拖拽或 Quick 'n Easy FTP Server 传输以保持换行符完整性。
汇编兼容性:63 个优化函数的编译难题
Quake 源码包含 21 个.s 文件,共 63 个手写汇编函数。这些代码使软件渲染版本帧率从 22.7fps 提升至 42.2fps,几乎翻倍。现代复现的核心挑战在于:
语法差异: 汇编文件使用 AT&T 语法(GNU 汇编器风格),而非 Microsoft 的 Intel 语法。ml.exe(Microsoft Macro Assembler)需要 Processor Pack 才能正确识别这些文件。
架构特定优化: Abrash 的代码深度针对 Pentium FPU 流水线设计,包括:
- 双流水线并行:利用 Pentium 的 U/V 整数流水线与 FPU 流水线同时执行
- FXCH 零周期指令:将 FPU 栈转为寄存器数组,避免 487 时代的 4 周期交换开销
- FDIV 重叠:在 39 周期的浮点除法执行期间,让整数单元绘制当前 8 像素跨度
- 自修改代码:R_DrawSurfaceBlock8_mip 系列函数在运行时将 colormap 基址硬编码到指令流,避免寄存器占用和额外 ADD 指令
关键函数性能贡献:
- D_DrawSpans8:+12.6 fps(墙面渲染)
- R_DrawSurfaceBlock8_mip*:+4.2 fps(光照贴图烘焙)
- D_Polyset*:+2.2 fps(模型渲染)
16 位 / 32 位边界:DOS 遗留与现代 Windows
Quake 源码同时承载 DOS 和 Win32 路径,产生以下兼容性问题:
内存模型冲突: 原始代码包含 DOS 实模式支持(VGA_UpdatePlanarScreen/VGA_UpdateLinearScreen),这些 16 位特定函数在现代 32 位 Windows 环境中需要条件编译排除。
FPU 精度控制: sys_dosa.s 和 sys_wina.s 中的 Sys_LowFPPrecision/Sys_HighFPPrecision 直接操作 FPU 控制字,这在现代操作系统中可能触发权限异常。
非 Intel CPU 支持: 将 id386 宏设为 0 可禁用所有汇编优化,但需要添加 nointel.c 到项目以提供 C 语言回退实现。这会产生与原始发布版本性能特征完全不同的二进制文件。
DirectX 版本迁移路径
原始 Quake 存在三个渲染后端:
- quake.exe:DOS 版本,使用模式 X VGA
- winquake.exe:Win32 软件渲染,使用 GDI
- glquake.exe:OpenGL 加速版本
现代复现通常聚焦于 winquake.exe,因为它代表了 1997 年的典型 Windows 游戏架构。若需 DirectX 支持,社区维护的 D3DQuake 项目提供了 Direct3D 7.0 移植参考,但这已超出原始 1997 年代码范畴。
可落地的复现参数与检查清单
虚拟机配置:
- 平台:VirtualBox 6.x/7.x
- 系统:Windows NT 4.0 Workstation 或 Windows 98SE
- 内存:64-128MB(符合 1997 年高端配置)
- 显示:1280x1024(VC++6 安装界面在高分屏显示异常,建议 800x600)
编译验证步骤:
- 获取 q1source.zip(来自 Quake Official Archive)
- 使用 WinRar 2.50 解压(保持 8.3 文件名兼容性)
- 打开 WinQuake.dsw,执行 "Rebuild All"
- 若汇编失败,检查 ml.exe 是否存在于 VC++6 的 bin 目录
- 复制 PmProXX.dll、WdirXX.dll 及 id1 数据目录到输出目录
- 运行
winquake.exe -wavonly +d_subdiv16 0 +timedemo demo1验证帧率
现代工具链替代方案: 对于需要在现代 Windows 上编译的场景,可考虑:
- 使用 Clang 或 GCC 配合 GNU as 替代 ml.exe(需转换 AT&T 语法)
- 用 SDL2 替换 Win32 特定 API 调用
- 保留汇编优化需内联汇编或单独编译为目标文件后链接
结语
复现 1997 年 Quake 编译环境不仅是对游戏史的考古,更是对软件工程可复现性的压力测试。从换行符敏感的.dsw 文件到 Pentium 特定的 FPU 流水线优化,每个环节都展示了遗留代码兼容性的复杂维度。对于维护 20 年以上历史代码库的团队,Fabien Sanglard 的这项工作提供了宝贵的参考框架:完整的依赖文档、版本锁定的工具链、以及可验证的输出基准。
资料来源:
- Fabien Sanglard, "Let's compile Quake like it's 1997!", 2026
- Fabien Sanglard, "How Michael Abrash doubled Quake framerate", 2026
- id Software, Quake source code (q1source.zip), 1999
内容声明:本文无广告投放、无付费植入。
如有事实性问题,欢迎发送勘误至 i@hotdrydog.com。