迈克尔·阿布拉什如何将《雷神之锤》帧率翻倍

Fabien Sanglard 新闻

摘要

分析表明,迈克尔·阿布拉什手工编写的汇编优化使《雷神之锤》的帧率相比纯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!

Fabien Sanglard

一份详细的指南,介绍如何重现使用Windows NT 4和Visual C++ 6等老式工具编译Quake的win32二进制文件的过程(就像1997年所做的那样)。

WinQuake 存在的原因及其工作原理

Fabien Sanglard

深入探讨创建 WinQuake(Quake 的 Windows 原生版本)的历史原因,以及它如何在 Windows 95 和 NT 上实现接近 DOS 版本的性能。