迈克尔·阿布拉什如何将《雷神之锤》帧率翻倍
摘要
分析表明,迈克尔·阿布拉什手工编写的汇编优化使《雷神之锤》的帧率相比纯C编译几乎翻倍。
暂无内容
查看缓存全文
缓存时间: 2026/05/16 03:33
# 迈克尔·阿布拉什如何让《雷神之锤》帧率翻倍
来源:https://fabiensanglard.net/quake_asm_optimizations/index.html
2026年2月14日
迈克尔·阿布拉什如何让《雷神之锤》帧率翻倍
---
随着1999年《雷神之锤》源代码的发布,附带了一份由约翰·卡马克编写的 `readme.txt` (https://github.com/id-Software/Quake/blob/master/readme.txt)。其中有一句话引起了我的好奇。
``
还需要 Masm 来构建汇编语言文件。可以通过修改 #define 仅用 C 代码构建,但软件渲染版本会损失近一半的速度。
``
《雷神之锤》凭借手工编写的汇编能快两倍?让我们来验证一下是否属实,看看它是如何工作的,以及哪些优化最为重要。
## 在我的机器上建立基准帧率
---
在对源码进行任何操作之前,我需要确定发布的 `winquake.exe` 版本在我的 Pentium MMX 233MHz 机器上的帧率是多少。
``
C:\winquake> winquake.exe -wavonly +d_subdiv16 0 +timedemo demo1
``
我禁用了 `d_subdiv16`,因为它没有 C 实现(这样会导致无法进行 C 与 ASM 的对比)。这使得引擎回退到使用 D_DrawSpans8 而不是 D_DrawSpans16(每 8 个像素进行透视采样,而不是 16 个)。`-wav` 是最快的音频后端(也称为 wq.bat 中的 "fastvid" 选项) (https://fabiensanglard.net/winquake)。
[](https://fabiensanglard.net/quake_asm_optimizations/42.3.png) 原始 winquake 以 42.3fps 完成 `timedemo demo1`。
## 使用 ASM 构建
---
按照《像1997年一样编译!》 (https://fabiensanglard.net/compile_like_1997) 中的步骤,我以发布模式构建了包含 ASM 优化的 `winquake.exe`。我真的希望 VC++6 编译器相对于 VC++4(id Software 在 1997 年发布 winquake 时使用的版本)没有显著改进[\[1\]](https://fabiensanglard.net/quake_asm_optimizations/index.html#footnote_1)。
``
C:\winquake> WinQuake_ASM.exe -wavonly +d_subdiv16 0 +timedemo demo1
``
[](https://fabiensanglard.net/quake_asm_optimizations/42.2.png) 我很欣慰地看到 `WinQuake_ASM.exe` (https://fabiensanglard.net/quake_asm_optimizations/WinQuake_fab_ASM.exe) 以几乎相同的帧率运行,42.2 fps。我走在正确的轨道上。
## 不使用 ASM 构建
---
正如约翰·卡马克所说,不使用 ASM 构建只需要在 `quakedef.h` 中将 `id386` 设置为 `0`。
[](https://fabiensanglard.net/quake_asm_optimizations/1.png) 这导致链接器出错,因为当时 VC6 项目意味着要在 Intel CPU 上运行。
[](https://fabiensanglard.net/quake_asm_optimizations/2.png) 我只需要将 [nointel.c](https://github.com/id-Software/Quake/blob/master/WinQuake/nonintel.c) 添加到项目中,就得到了一个可执行文件。
[](https://fabiensanglard.net/quake_asm_optimizations/nointel.c.png)
## 没有 ASM 优化的《雷神之锤》
---
在成功发布构建后,是时候运行 `WinQuake_No_ASM.exe` (https://fabiensanglard.net/quake_asm_optimizations/WinQuakeNo_ASM.exe) 了。
``
C:\winquake> WinQuake_No_ASM.exe -wavonly +d_subdiv16 0 +timedemo demo1
``
[](https://fabiensanglard.net/quake_asm_optimizations/22.7.png) 天哪!游戏确实以 **22.7fps** 运行,而不是 **42.2fps**!正如约翰·卡马克警告的那样,如果没有迈克尔·阿布拉什的优化,雷神之锤的帧率会减半!
## 深入汇编
---
《雷神之锤》中有大量的汇编代码。总共,grep 找到了分布在 21 个文件中的 63 个函数。
``
$ find . -name "*.s" | wc -l
21
``
``
$ find . -name "*.s" -exec grep -H ".globl C(" {} \;
./server/worlda.s:.globl C(SV_HullPointContents)
./server/math.s:.globl C(BoxOnPlaneSide)
./client/d_copy.s:.globl C(VGA_UpdatePlanarScreen)
./client/d_copy.s:.globl C(VGA_UpdateLinearScreen)
./client/d_draw.s:.globl C(D_DrawSpans8)
./client/d_draw.s:.globl C(D_DrawZSpans)
./client/surf16.s:.globl C(R_Surf16Start)
./client/surf16.s:.globl C(R_DrawSurfaceBlock16)
./client/surf16.s:.globl C(R_Surf16End)
./client/surf16.s:.globl C(R_Surf16Patch)
./client/d_scana.s:.globl C(D_DrawTurbulent8Span)
./client/r_drawa.s:.globl C(R_ClipEdge)
./client/d_parta.s:.globl C(D_DrawParticle)
./client/d_polysa.s:.globl C(D_PolysetCalcGradients)
./client/d_polysa.s:.globl C(D_PolysetRecursiveTriangle)
./client/d_polysa.s:.globl C(D_PolysetAff8Start)
./client/d_polysa.s:.globl C(D_PolysetDrawSpans8)
./client/d_polysa.s:.globl C(D_PolysetAff8End)
./client/d_polysa.s:.globl C(D_Aff8Patch)
./client/d_polysa.s:.globl C(D_PolysetDraw)
./client/d_polysa.s:.globl C(D_PolysetScanLeftEdge)
./client/d_polysa.s:.globl C(D_PolysetDrawFinalVerts)
./client/d_polysa.s:.globl C(D_DrawNonSubdiv)
./client/sys_wina.s:.globl C(MaskExceptions)
./client/sys_wina.s:.globl C(unmaskexceptions)
./client/sys_wina.s:.globl C(Sys_LowFPPrecision)
./client/sys_wina.s:.globl C(Sys_HighFPPrecision)
./client/sys_wina.s:.globl C(Sys_PushFPCW_SetHigh)
./client/sys_wina.s:.globl C(Sys_PopFPCW)
./client/sys_wina.s:.globl C(Sys_SetFPCW)
./client/math.s:.globl C(Invert24To16)
./client/math.s:.globl C(TransformVector)
./client/math.s:.globl C(BoxOnPlaneSide)
./client/d_draw16.s:.globl C(D_DrawSpans16)
./client/r_aclipa.s:.globl C(R_Alias_clip_bottom)
./client/r_aclipa.s:.globl C(R_Alias_clip_top)
./client/r_aclipa.s:.globl C(R_Alias_clip_right)
./client/r_aclipa.s:.globl C(R_Alias_clip_left)
./client/snd_mixa.s:.globl C(SND_PaintChannelFrom8)
./client/snd_mixa.s:.globl C(Snd_WriteLinearBlastStereo16)
./client/r_aliasa.s:.globl C(R_AliasTransformAndProjectFinalVerts)
./client/d_spr8.s:.globl C(D_SpriteDrawSpans)
./client/r_edgea.s:.globl C(R_EdgeCodeStart)
./client/r_edgea.s:.globl C(R_InsertNewEdges)
./client/r_edgea.s:.globl C(R_RemoveEdges)
./client/r_edgea.s:.globl C(R_StepActiveU)
./client/r_edgea.s:.globl C(R_GenerateSpans)
./client/r_edgea.s:.globl C(R_EdgeCodeEnd)
./client/r_edgea.s:.globl C(R_SurfacePatch)
./client/surf8.s:.globl C(R_Surf8Start)
./client/surf8.s:.globl C(R_DrawSurfaceBlock8_mip0)
./client/surf8.s:.globl C(R_DrawSurfaceBlock8_mip1)
./client/surf8.s:.globl C(R_DrawSurfaceBlock8_mip2)
./client/surf8.s:.globl C(R_DrawSurfaceBlock8_mip3)
./client/surf8.s:.globl C(R_Surf8End)
./client/surf8.s:.globl C(R_Surf8Patch)
./client/sys_dosa.s:.globl C(MaskExceptions)
./client/sys_dosa.s:.globl C(unmaskexceptions)
./client/sys_dosa.s:.globl C(Sys_LowFPPrecision)
./client/sys_dosa.s:.globl C(Sys_HighFPPrecision)
./client/sys_dosa.s:.globl C(Sys_PushFPCW_SetHigh)
./client/sys_dosa.s:.globl C(Sys_PopFPCW)
./client/sys_dosa.s:.globl C(Sys_SetFPCW)
``
作为对比,DOOM 只有两个 `.asm` 文件和三个函数来加速引擎。
其中许多函数可以忽略。有些函数做了一些不能在 C 中完成的事情,比如设置浮点单元精度或设置高精度计数器()。有些未被使用()。有些是重复的(一个用于服务器,一个用于客户端)。有些优化使用了自修改代码,需要标记以便将 `.text` 区域从 `r` 更新为 `rw` 并进行修补()。
``
$ find . -name "*.s" -exec grep -H ".globl C(" {} \;
./server/worlda.s:.globl C(SV_HullPointContents)
./server/math.s:.globl C(BoxOnPlaneSide) // 来自 ./client/math.s 的重复
./client/d_copy.s:.globl C(VGA_UpdatePlanarScreen) // DOS
./client/d_copy.s:.globl C(VGA_UpdateLinearScreen) // DOS
./client/d_draw.s:.globl C(D_DrawSpans8)
./client/d_draw.s:.globl C(D_DrawZSpans)
./client/surf16.s:.globl C(R_Surf16Start)
./client/surf16.s:.globl C(R_DrawSurfaceBlock16) // 实验性 16 位渲染
./client/surf16.s:.globl C(R_Surf16End)
./client/surf16.s:.globl C(R_Surf16Patch)
./client/d_scana.s:.globl C(D_DrawTurbulent8Span)
./client/r_drawa.s:.globl C(R_ClipEdge)
./client/d_parta.s:.globl C(D_DrawParticle)
./client/d_polysa.s:.globl C(D_PolysetCalcGradients)
./client/d_polysa.s:.globl C(D_PolysetRecursiveTriangle)
./client/d_polysa.s:.globl C(D_PolysetAff8Start)
./client/d_polysa.s:.globl C(D_PolysetDrawSpans8)
./client/d_polysa.s:.globl C(D_PolysetAff8End)
./client/d_polysa.s:.globl C(D_Aff8Patch)
./client/d_polysa.s:.globl C(D_PolysetDraw)
./client/d_polysa.s:.globl C(D_PolysetScanLeftEdge)
./client/d_polysa.s:.globl C(D_PolysetDrawFinalVerts)
./client/d_polysa.s:.globl C(D_DrawNonSubdiv)
./client/sys_wina.s:.globl C(MaskExceptions)
./client/sys_wina.s:.globl C(unmaskexceptions)
./client/sys_wina.s:.globl C(Sys_LowFPPrecision)
./client/sys_wina.s:.globl C(Sys_HighFPPrecision)
./client/sys_wina.s:.globl C(Sys_PushFPCW_SetHigh)
./client/sys_wina.s:.globl C(Sys_PopFPCW)
./client/sys_wina.s:.globl C(Sys_SetFPCW)
./client/math.s:.globl C(Invert24To16)
./client/math.s:.globl C(TransformVector)
./client/math.s:.globl C(BoxOnPlaneSide)
./client/d_draw16.s:.globl C(D_DrawSpans16)
./client/r_aclipa.s:.globl C(R_Alias_clip_bottom)
./client/r_aclipa.s:.globl C(R_Alias_clip_top)
./client/r_aclipa.s:.globl C(R_Alias_clip_right)
./client/r_aclipa.s:.globl C(R_Alias_clip_left)
./client/snd_mixa.s:.globl C(SND_PaintChannelFrom8)
./client/snd_mixa.s:.globl C(Snd_WriteLinearBlastStereo16)
./client/r_aliasa.s:.globl C(R_AliasTransformAndProjectFinalVerts)
./client/d_spr8.s:.globl C(D_SpriteDrawSpans)
./client/r_edgea.s:.globl C(R_EdgeCodeStart)
./client/r_edgea.s:.globl C(R_InsertNewEdges)
./client/r_edgea.s:.globl C(R_RemoveEdges)
./client/r_edgea.s:.globl C(R_StepActiveU)
./client/r_edgea.s:.globl C(R_GenerateSpans)
./client/r_edgea.s:.globl C(R_EdgeCodeEnd)
./client/r_edgea.s:.globl C(R_SurfacePatch)
./client/surf8.s:.globl C(R_Surf8Start)
./client/surf8.s:.globl C(R_DrawSurfaceBlock8_mip0)
./client/surf8.s:.globl C(R_DrawSurfaceBlock8_mip1)
./client/surf8.s:.globl C(R_DrawSurfaceBlock8_mip2)
./client/surf8.s:.globl C(R_DrawSurfaceBlock8_mip3)
./client/surf8.s:.globl C(R_Surf8End)
./client/surf8.s:.globl C(R_Surf8Patch)
./client/sys_dosa.s:.globl C(MaskExceptions)
./client/sys_dosa.s:.globl C(unmaskexceptions)
./client/sys_dosa.s:.globl C(Sys_LowFPPrecision)
./client/sys_dosa.s:.globl C(Sys_HighFPPrecision)
./client/sys_dosa.s:.globl C(Sys_PushFPCW_SetHigh)
./client/sys_dosa.s:.globl C(Sys_PopFPCW)
./client/sys_dosa.s:.globl C(Sys_SetFPCW)
``
这仍然留下了 32 个涉及数学、声音、渲染和绘制的方法。R_ 和 D_ 之间的区别并不明显。R_ 代码负责 *绘制什么*。D_ 代码负责 *如何绘制*。
``
//******* 绘制 *******
./client/d_spr8.s:.globl C(D_SpriteDrawSpans) // 绘制面向相机的精灵
./client/d_draw.s:.globl C(D_DrawSpans8) // 世界绘制 8 像素透视
./client/d_draw.s:.globl C(D_DrawZSpans) // 世界写入 Z 缓冲区
./client/d_draw16.s:.globl C(D_DrawSpans16) // 世界绘制 16 像素透视
./client/d_scana.s:.globl C(D_DrawTurbulent8Span)
./client/d_parta.s:.globl C(D_DrawParticle)
./client/d_polysa.s:.globl C(D_PolysetCalcGradients) // 所有 polyset 用于
./client/d_polysa.s:.globl C(D_PolysetRecursiveTriangle) // 别名模型渲染。
./client/d_polysa.s:.globl C(D_PolysetDrawSpans8)
./client/d_polysa.s:.globl C(D_PolysetDraw)
./client/d_polysa.s:.globl C(D_PolysetScanLeftEdge)
./client/d_polysa.s:.globl C(D_PolysetDrawFinalVerts)
./client/d_polysa.s:.globl C(D_DrawNonSubdiv) // 也是模型绘制
//******* 数学 *******
./client/math.s:.globl C(TransformVector)
./client/math.s:.globl C(BoxOnPlaneSide)
./server/worlda.s:.globl C(SV_HullPointContents)
//******* 声音 *******
./client/snd_mixa.s:.globl C(SND_PaintChannelFrom8)
./client/snd_mixa.s:.globl C(Snd_WriteLinearBlastStereo16)
//******* 渲染 *******
./client/r_drawa.s:.globl C(R_ClipEdge)
./client/r_aclipa.s:.globl C(R_Alias_clip_bottom)
./client/r_aclipa.s:.globl C(R_Alias_clip_top)
./client/r_aclipa.s:.globl C(R_Alias_clip_right)
./client/r_aclipa.s:.globl C(R_Alias_clip_left)
./client/r_aliasa.s:.globl C(R_AliasTransformAndProjectFinalVerts)
./client/r_edgea.s:.globl C(R_InsertNewEdges)
./client/r_edgea.s:.globl C(R_RemoveEdges)
./client/r_edgea.s:.globl C(R_StepActiveU)
./client/r_edgea.s:.globl C(R_GenerateSpans)
./client/surf8.s:.globl C(R_DrawSurfaceBlock8_mip0) // 表面缓存生成
./client/surf8.s:.globl C(R_DrawSurfaceBlock8_mip1) // 表面缓存生成
./client/surf8.s:.globl C(R_DrawSurfaceBlock8_mip2) // 表面缓存生成
./client/surf8.s:.globl C(R_DrawSurfaceBlock8_mip3) // 表面缓存生成
``
在进一步深入之前,下一步是量化每个函数对从 22.7fps 提升到 42.2fps 的贡献有多大。为此,我修改了引擎,一次只启用一个 ASM 函数,并反复运行相同的 timedemo。
| 函数名 | 帧率 (fps) 增益 |
|---------|----------------|
| D_DrawSpans8 | 12.6 |
| R_DrawSurfaceBlock8_mip* | 4.2 |
| D_Polyset* | 2.2 |
| D_DrawZSpans | 0.2 |
| D_DrawParticle | 0.1 |
| 其他 | 0.6 |
| **总计:** | **19.5** |
毫不意外,最重要的优化位于底层绘制例程中:`D_DrawSpans8` 用于渲染墙壁,`R_DrawSurfaceBlock8X` 用于组合纹理和光照贴图到表面,以及 `D_Polyset*` 用于绘制模型。其余的在我的(相当粗略的)基准测试中几乎没有任何影响。
[](https://fabiensanglard.net/quake_asm_optimizations/chart.svg) *每个 ASM 函数对帧率从 22.7fps 提升到 42.2fps 的贡献程度。* Polyset* 函数相互交织,无法单独切换为 C/ASM。它们必须全部是 C 或全部是 ASM。
我发现的 ASM 优化通常涉及循环展开、自修改代码、避免分支预测错误、利用 Pentium FPU 流水线隐藏延迟,以及创建“重叠”使得 Pentium U/V 流水线和 FPU 流水线并行执行指令。
以下是一些详细的函数。对于那些想更深入探讨这个兔子洞的人,我建议阅读 *面向 Intel 32 位处理器的优化(1994 年 2 月)*[\[2\]](https://fabiensanglard.net/quake_asm_optimizations/index.html#footnote_2),其中广泛介绍了 Pentium。但要警告,它比 20 克褪黑素还要强力。
## TransformVector
---
`TransformVector` 函数是 P5 FPU 的一个很好的介绍。它是一个简单的矩阵-向量乘法,广泛用于将世界多边形、模型/别名多边形和精灵投影到屏幕空间。
``
typedef float vec_t;
typedef vec3_t[3];
vec3_t vpn, vright, vup;
#define DotProduct(x,y) (x[0]*y[0]+x[1]*y[1]+x[2]*y[2])
void TransformVector (vec3_t in, vec3_t out) {
out[0] = DotProduct(in,vright);
out[1] = DotProduct(in,vup);
out[2] = DotProduct(in,vpn);
}
``
让我们看看汇编代码。我将 mabrash 的 asm 保留在 AT&T 符号中[\[3\]](https://fabiensanglard.net/quake_asm_optimizations/index.html#footnote_3) 在左边。右边是 VC6 生成的代码,采用 Intel 符号,由 Ninja 反编译。
``
// Abrash 版本
.globl C(TransformVector)
movl in(%esp),%eax
movl out(%esp),%edx
flds (%eax)
fmuls C(vright)
flds (%eax)
fmuls C(vup)
flds (%eax)
fmuls C(vpn)
flds 4(%eax)
fmuls C(vright)+4
flds 4(%eax)
fmuls C(vup)+4
flds 4(%eax)
fmuls C(vpn)+4
fxch %st(2)
faddp %st(0),%st(5)
faddp %st(0),%st(3)
faddp %st(0),%st(1)
flds 8(%eax)
fmuls C(vright)+8
flds 8(%eax)
fmuls C(vup)+8
flds 8(%eax)
fmuls C(vpn)+8
fxch %st(2)
faddp %st(0),%st(5)
faddp %st(0),%st(3)
faddp %st(0),%st(1)
fstps 8(%edx)
fstps 4(%edx)
fstps (%edx)
ret
``
``
// VC6 输出
float* TransformVector(float* a1, float* a2)
mov eax, dword [esp+0x4 {a1}]
mov ecx, dword [esp+0x8 {a2}]
fld st0, dword [0x2970] // vright.x
fmul st0, dword [eax]
fld st0, dword [0x2978] // vright.y
fmul st0, dword [eax+0x8]
faddp st1, st0
fld st0, dword [0x2974] // vright.z
fmul st0, dword [eax+0x4]
faddp st1, st0
fstp dword [ecx], st0
fld st0, dword [0x2974] // vup.x
fmul st0, dword [eax]
fld st0, dword [0x297c] // vup.y
fmul st0, dword [eax+0x8]
faddp st1, st0
fld st0, dword [0x2978] // vup.z
相似文章
打造一台1997年的Quake PC:对GLquake进行基准测试
一位开发者讲述了他组装一台配备3dfx Voodoo显卡的复古PC,并对GLQuake进行基准测试的过程,同时讨论了硬件的小问题和性能印象。
打造一台1997年玩《雷神之锤》的PC:VQuake性能基准测试
一篇详细的回顾文章,讲述如何组装一台1997年时代的《雷神之锤》PC,并对硬件加速版VQuake进行基准测试,重点介绍Rendition Verite 1000显卡及其独特功能,包括双线性过滤和全亮度支持。
打造一台1997年雷神之锤PC:雷神之锤基准测试
针对各种1990年代CPU和配置下雷神之锤性能的详细技术分析,比较英特尔、Cyrix、AMD芯片以及DOS和Windows 95下的内存类型。
像1997年那样编译Quake!
一份详细的指南,介绍如何重现使用Windows NT 4和Visual C++ 6等老式工具编译Quake的win32二进制文件的过程(就像1997年所做的那样)。
WinQuake 存在的原因及其工作原理
深入探讨创建 WinQuake(Quake 的 Windows 原生版本)的历史原因,以及它如何在 Windows 95 和 NT 上实现接近 DOS 版本的性能。