FABIEN SANGLARD'S WEBSITE ABOUT CONTACT RSS GIVE Mar 04, 2025

为什么 fastDOOM 这么快?

在 2024 年的冬天,我修复了一台 IBM PS/1 486-DX2 66Mhz "Mini-Tower",型号 2168。它是我青少年时期一直想要但买不起的电脑。文字无法表达我在使用这台机器时的喜悦之情。

一旦它能够启动,我就对我想运行的那个软件进行了基准测试。

C:\DOOM>doom.exe -timedemo demo1
timed 1710 gametics in 2783 realtics 

DOOM 没有直接给出 fps。你需要做一些数学运算才能得到帧率。在这个例子中,就是 1710/2783*35 = 21.5 fps。对于 1993 年 12 月能用钱买到的最好的机器来说,这是一个光荣的性能表现(合理范围内)(配置芯片组视频硬盘1硬盘2speedsys)。

我原本打算在止痛药的帮助下玩游戏,直到我听说了 fastDOOM。我通常不喜欢移植版本,因为它们往往会添加不具有凝聚力的功能(除了梦幻般的 Chocolate DOOM),但出于好奇,我还是试了一下。

C:\DOOM>fdoom.exe -timedemo demo1
Timed 1710 gametics in 1988 realtics. FPS: **30.1**

速度提高了 30%,而且没有任何功能被删减[1]!在像 doom2 的 demo1 这样要求很高的地图上,增益甚至更高,从 16.8 fps 提升到 24.9 fps。速度提高了 48%!

我没想到 DOOM 还有这么大的优化空间。显然,在一年内完成发售没有留下多少时间进行优化。我必须了解这种魔术是如何发生的。

一段历史

在深入研究 fastDOOM 之前,让我们了解一下代码的来源。DOOM 最初是在 NeXT Workstation 上开发的。游戏的结构易于移植,大部分代码位于核心中,周围是执行 I/O 的小型子系统。

来源: Game Engine Black Book: DOOM

在开发过程中,DOS I/O 由 id Software 编写。这成为了 DOOM 的商业版本。但该版本在 1997 年无法开源,因为它依赖于一个名为 DMX 的专有声音库。

最终开源的是 Linux 版本,由 Bernd Kreimeier 在他编写一本解释引擎的书籍项目时清理过。

DOOM 的 DOS 版本是通过使用 Linux 的核心、Heretic I/O 和 APODMX (Apogee Sound wrapper) 来模拟 DMX 重建的。由于 Heretic 使用视频模式 13h,而 DOOM 使用视频模式 Y,因此图形 I/O (i_ibm.c) 是从 DOOM.EXE 反汇编逆向工程得到的。这就是社区获得 PCDOOM v2[2] 的方式。

fastDOOM 的起点是 PCDOOM v2。

          ┌───────────────┐               
          │ NeXTStep DOOM │               
          └─────┬────┬────┘               
             │  │                 
             │  │                 
             │  │                 
     ┌────────────┐ │  │ ┌──────┐   ┌─────────┐   
     │ Linux DOOM │◄─┘  └─►│ DOOM ├─────►│ Heretic │   
     └──────┬─────┘     └──────┘   └────┬────┘   
         │          ⁞       │      
         │          ▼       │      
         │       ┌──────────┐     │      
         └─────────────►│ PCDOOMv2 │◄────────┘      
                └─────┬────┘           
                   ▼             
                ┌──────────┐           
                │ fastDOOM │           
fastDoom genealogy       └──────────┘
──────────────────

性能概况

Victor "Viti95" Nieto 编写了发行说明,描述了每个版本的性能改进,但他似乎更热衷于让 FDOOM.EXE 变得更好,而不是详细说明他是如何做到的。

为了了解性能随时间推移的演变概况,我下载了所有 52 个版本的 fastDOOM、PCDOOMv2 和原始的 DOOM.EXE,编写了一个 go 程序来生成一个 RUN.BAT,在所有这些版本上运行 -timedemo demo1,并使用 mTCP 的 NETDRIVE 挂载它们。

我选择使用 DOOM.WAD 进行 timedemo,开启声音并设置屏幕尺寸 = 10(全屏带状态栏)。经过几个小时的霰弹枪和小恶魔的折磨后,我已经运行了整个套件五次,并使用 chart.js 绘制了平均 fps。

这张图表首先可以排除的是,fastDOOM 的改进主要归功于使用现代编译器。PCDOOMv2 是使用 OpenWatcom 2 构建的,但与 DOOM.EXE 相比,只有略微的改进。

git 考古学

除了经常发布之外,Viti95 还表现出出色的 git 纪律,即一次提交只做一件事,并且每个版本都进行了标记。fastDOOM git 历史由 3,042 次提交组成,这允许对每个功能进行基准测试。

我编写了另一个 go 程序来构建每次提交。我将跳过处理许多构建系统更改的血腥细节(尤其从 DOS 到 Linux)。一个小时后,我有了我写过的最丑陋的程序和 3,042 个 DOOM.EXE。我很高兴看到构建几乎从未中断过。

绘制文件大小的图表显示,早期的工作是通过清理和删除代码来精简代码。在 bf0e983 (构建 239,其中删除了录音功能)、5f38323 (构建 0340,其中删除了错误代码字符串) 和 8b9cac5 (构建 1105,其中 TASM 被 NASM 替换) 时,出现了主要的下降。

深入研究

对所有构建进行 timedemo 会花费很长时间(3042x1.5/60/24 * 3 次传递 = 9 天),因此我专注于获得大部分速度提升的版本。我编写了另一个 go 程序来生成一个 .BAT 文件,用于运行 v0.1v0.6v0.8v0.9.2v0.9.7 中所有提交的 timedemo。我使用 mTCP 挂载了 1.4 GiB 的 FDOOM.EXE 并运行了它。这花了一段时间,因为具有 200+ 提交的版本运行时为 8 小时/次传递。

fastDOOM v0.1

此版本包含 220 次提交

$ git log --reverse --oneline "0.1" | wc -l
   220

图表可点击且可悬停

v0.1 的 MPV 补丁无疑是构建 36 (e16bab8)。"Crispy optimization" 将状态栏百分比渲染转换为无操作,如果它们没有改变。这可以防止渲染到临时缓冲区并blit 到屏幕,总共提升了 2 fps。起初我简直不敢相信。我假设我的工具链有一个 bug。但是在 PCDOOMv2 上 cherry-pick 这个补丁证实了巨大的速度提升。

接下来是构建 167 (a9359d5),它通过宏内联 FixedDiv。

在接近尾声时,我们看到了一系列优化,授予了 0.5 fps。构建 207 (9bd3f20):PSX Doom 优化,它优化了 BSP 的遍历方式。构建 212 (dc0f48e) "Inlined R_MakeSpans",它渲染水平表面。

总的来说,这个版本删除了很多代码(50% 的提交是删除),这可能有助于拥抱我机器的 486 缓存线。

git log --reverse --oneline "0.1" | grep -i -E "remove|delete" | wc -l
   100

不知何故,我的一个补丁 进入了 fastDOOM。可能是在我写 Black Book 的时候?我完全不记得写过这个!

fastDOOM v0.6

此版本包含 33 次提交。

$ git log --reverse --oneline "0.5"^.."0.6" | wc -l 
   33

图表可点击且可悬停

在许多小的优化(你好 GbaDOOM 341)) 中,MVP 优化如下。构建 342 (22819fd) 跳过渲染不需要的 visplanes。构建 359 (40e0d4b) 删除了玩家指针的一个级别间接寻址。构建 360 (ccd296f) 双倍降低间接寻址。构建 369 (f29e665) 内联屏幕空间线分割器。

fastDOOM v0.8

此版本包含 282 次提交

$ git log --reverse --oneline "0.7"^.."0.8" | wc -l
   282

声音系统有点不稳定,所以我不得不不发出声音地进行 timedemo,然后对 fps 进行归一化。此外,v0.8 的重点似乎是文本模式渲染器,因此在构建 670 (a92c67f) 和构建 730 (c3f5f50) 中发生了两次回归,其中 Crispy 优化消失了。

图表可点击且可悬停

MVPs: 构建 792 (f279b7d):每个渲染器一个可执行文件(FDOOM.EXEFDOOM13H.EXE 等)。构建 793 (1874ee8):禁用编译器的调试功能。构建 796 (6aae724):恢复 Crispy 优化。构建 794 (1366ebf):尽可能少地编译代码。

fastDOOM v0.9.2

此版本包含 110 次提交

$ git log --reverse --oneline "0.9.1"^.."0.9.2" | wc -l
   110

图表可点击且可悬停

MVPs: 构建 1639 (ae2a951):优化 skyflatnum 比较。构建 1645 (0730cdc):优化 Mode Y 的 R_DrawColumn。构建 1646 (17c9e83):清理 R_DrawSpan 代码。

fastDOOM v0.9.7

此版本包含 293 次提交

$ git log --reverse --oneline "0.9.6"^.."0.9.7" | wc -l
   294

尽管多次运行基准测试,但我无法减少此版本的噪声。

图表可点击且可悬停

MVPs: 构建 1941 (0688235):测试 x86 ASM 更改。构建 1943 (f326e73):为 386SX 添加 CPU 选择 + CR2 优化。构建 1944 (a836abb):为 R_DrawSpan386SX 添加 ESP 优化。构建 2000 (3432590):添加 ASM 中渲染 fuzz 列的基本代码。构建 2031 (0edab46):每个循环删除一个 CMP 比较(ken silverman 的优化)?

Mode 13h vs Mode Y

fastDOOM 探索了许多提高速度的方法,适用于各种 CPU(386、486、Pentium、Cyrix)和视频总线(ISA、VLB、PCI)。一个在我的机器上不起作用的优化是使用视频模式 13h 而不是模式 Y。

在模式 13h 中,数据向 VGA 的四个 VRAM 存储体的分派是在硬件中完成的。对于 CPU 来说,VRAM 看起来像一个单一的线性 320x200 帧缓冲区。不方便的是,您不能在 VRAM 中进行双缓冲,因此您必须在 RAM 中进行,这意味着字节被写入两次。首先写入 RAM 中的帧缓冲区。然后第二次发送到 VRAM 时。此外,引擎必须阻止 VSYNC。

Mode 13h                                                
────────       RAM          VRAM (VGA card)         SCREEN       
       ┌───────────────────┐   ┌───────────────────┐    ┌───────────────────┐   
       │ ┌───────────────┐ │   │          │    │          │   
       │ │ framebuffer 1 │ │   │          │    │          │   
       │ └───────────────┘ │   │          │    │          │   
       │ ┌───────────────┐ │   │ ┌───────────────┐ │    │          │   
  CPU ────►│ │ framebuffer 2 │ ├────► │ │framebuffer(fb)│ ├──────►│          │   
       │ └───────────────┘ │   │ └───────────────┘ │    │          │   
       │ ┌───────────────┐ │   │          │    │          │   
       │ │ framebuffer 3 │ │   │          │    │          │   
       │ └───────────────┘ │   │          │    │          │   
       └───────────────────┘   └───────────────────┘    └───────────────────┘   

模式 Y 允许程序员单独访问 VGA 存储体。这允许在 VRAM 中进行三缓冲。此外,它具有一次性将字节直接写入 VRAM 的优点。目标存储体必须由开发人员通过非常慢的 OUT 指令手动选择,但这允许通过闩锁[3] 同时写入两个 VGA 存储体来水平复制像素(这免费提供了低细节模式)。另一个不便是,它使绘制隐形 Specter 的速度慢得多,因为它需要从 VRAM 中读回。

Mode Y                                                
───────                  VRAM (VGA card)         SCREEN     
                    ┌───────────────────┐    ┌───────────────────┐ 
                    │ ┌───────────────┐ │    │          │ 
                    │ │fb1 | fb2 | fb3│ │    │          │ 
                    │ └───────────────┘ │    │          │ 
                    │ ┌───────────────┐ │    │          │ 
                    │ │fb1 | fb2 | fb3│ │    │          │ 
  CPU ──────────────────────────────► │ └───────────────┘ ├──────►│          │ 
                    │ ┌───────────────┐ │    │          │ 
                    │ │fb1 | fb2 | fb3│ │    │          │ 
                    │ └───────────────┘ │    │          │ 
                    │ ┌───────────────┐ │    │          │ 
                    │ │fb1 | fb2 | fb3│ │    │          │ 
                    │ └───────────────┘ │    │          │ 
                    └───────────────────┘    └───────────────────┘  

对于具有快速 CPU 和总线(100+ Mhz/Pentium 和 VLB/PCI)的机器,其中显卡不太可能很好地处理 OUT 指令,模式 13h 更好。对于“慢速 CPU”,通过模式 Y 一次性将数据写入 VRAM 更快。

无论如何,Doom 使用了模式 Y。

DOOM 使用 320200256 VGA 模式,这与 MCGA 模式略有不同(它不能在配备 MCGA 的机器上运行)。我以类似于 Michael Abrash 的 "Mode X" 的交错平面模式访问帧缓冲区,但仍然在 200 条扫描线上而不是 240 条扫描线上(更少的像素 == 更快的更新速率)。DOOM 在三个显示页面之间循环。如果只使用两个,它将不得不同步到 VBL 以避免可能的显示闪烁。如果您仔细观察 HOM 效果,您应该会看到三个不同的图像在循环。

  • John Carmack[4] (镜像) John 当年因为我使用 Mode-Y 而骂我的另一个原因是图形团队使用的工具(Deluxe Paint)只支持 320x200(而 Mode-X 是 320x240)。 e...@agora.rdrop.com (Ed Hurtley) 写道: >Check, please... In case you haven't hit ESC ever, the Options menu >has a Low/High resolution toggle... Low is 320x200, High is >640x400, with the border graphics (the score bar, menu, etc...) are >still 320x200... (Just the same graphics files) Low detail is 160*200 in the view screen. This is done by setting two bits in the mapmask register whenever the texturing functions are writing to video memory, causing two pixels to be set for each byte written. ui...@freenet.Victoria.BC.CA (Ben Morris) wrote: >John, >You're using a planar graphics system for a bitmapped game that >updates the entire screen at a respectable framrate on a 486/66? Its planar, but not bit planar (THAT would stink). Pixels 0,4,8 are in plane 0, pixels 1,5,9 are in plane 1, etc. >That's pretty incredible. I would have thought all the over- >head for programming the VGA registers would kill that >possibility. The registers don't need to be programed all that much. The map mask register only needs to be set once for each vertical column, and four times for each horizontal row (I step by four pixels in the inner loop to stay on the same plane, then increment the start pixel and move to the next plane). It is still a lot of grief, and it polutes the program quite a bit, but texture mapping directly to the video memory gives you a fair amount of extra speed (10% - 15%) on most video cards because the video writes are interleaved with main memory accesses and texture calculations, giving the write time to complete without stalling. Going to that trouble also gets a perfect page flip, rather than the tearing you get with main memory buffering.
  • John Carmack[5] (镜像) Heretic 于 1994 年发布。硬件已经发展到使模式 13h[6][7] 更具吸引力,因此 Raven 对 DOOM 引擎进行了修改以达到这种效果。PCDoom v2 使用了 Heretic I/O,但使用模式 Y 重新实现了视频 I/O。最后,fastDOOM 通过提供几个可执行文件 FDOOM.EXEFDOOM13H.EXEFDOOMVBD.EXE 为用户提供了选择。 DOOM 新闻稿 beta 版(1993 年 10 月)使用了 Mode 13h,所以我假设他们切换到 Mode Y 是为了提高较慢机器(低细节)的性能。我想知道为什么他们也没有实现所谓的“土豆模式”,该模式使用单个 8 位写入 VRAM 来写入四个像素。在 FastDoom 中,我重新引入了 Mode 13h,因为 Heretic/Hexen 对此模式有更好的优化 ASM 渲染代码。后来,我能够将这种方法部分移植到 Mode Y 中的列渲染,从而使性能提高了 5% 到 7%。根据我的测试,486 CPU 的最佳模式是 VESA 直接模式(适用于 320x200 的 FDOOMVBD.EXE)。此模式结合了 Mode Y 的优势和 Heretic 的优化渲染代码,同时避免了任何 OUT 指令 - 除了一个切换缓冲区的指令,该指令仅在每次渲染帧时执行一次。唯一的缺点是它需要具有启用 LFB 的 VLB 或 PCI 显卡,并且在低细节和土豆细节模式下性能较慢。
  • 与 Viti95 的对话 Viti95 在校对过程中进一步阐述了 fastDOOM 模式 13h。 在 FastDoom 中,Mode 13h 在 RAM 中使用单个帧缓冲区,在整个场景渲染完成后将其复制到 VRAM。不强制执行 Vsync,这可能会导致闪烁。有两种方法可以将后缓冲区复制到 VRAM,针对不同的总线速度进行了优化。对于慢速总线(8 位 ISA),使用差分复制方法,仅传输修改后的像素。这种方法涉及许多分支,但总体上速度更快,因为分支的成本低于过度的总线传输。对于更快的总线(16 位 ISA、VLB、PCI 等),使用 REP MOVS 指令执行完整的后缓冲区复制,当总线带宽足够时,这种方法很有效。
  • 与 Viti95 的对话 更多无效的优化

我赞赏看到的另一个探索领域是 OpenWatcom 的处理器特定标志(4r/4s vs 3r/3s)[8]。wcc386 的 386 和 486 标志都经过尝试,但最终停止使用,因为 386 版本似乎总是更快。

我为 FastDoom 制定的目标之一是将编译器从 OpenWatcom v2 切换到 DJGPP (GCC),这已被证明可以使用相同的源代码生成更快的代码。或者,如果有人可以改进 OpenWatcom v2 以缩小性能差距,那就太好了。

  • 与 Viti95 的对话 总体印象

Victor Nieto 完成了多么出色的工作!如果软件会死于一千次削减,那么 Viti95 用三千次优化使 fastDOOM 变得非常棒!他不仅利用了现有的改进(crispy、psx、gba、Lee Killough),他还提出了许多新的改进,并产生了如此多的炒作,以至于即使是 Ken Silverman(Duke3D build 引擎的作者)也来参与[9]。我向你致敬,Victor!

参考文献

^| [1]| 来自 Viti95 的说明: 已删除操纵杆和网络游戏支持,因此它不是一个完全功能完好的移植 ^^(人们仍然试图说服我恢复网络游戏)。 ---|---|--- ^| [2]| DOOM engine: gamesrc-ver-recreation ^| [3]| Game Engine Black Book: Wolfenstein 3D ^| [4]| Doom graphics modes usenet ^| [5]| Doom graphics modes usenet ^| [6]| [Doom vs Heretic VGA performance difference](https://fabiensanglard.net/fastdoom/< https:/www.vogons.org/viewtopic.php?t=61839>) ^| [7]| Doom in DOS: Original vs Source Ports ^| [8]| OpenWatcom documentation ^| [9]| 来自 Viti95 的说明: Ken Silverman 的一些想法和代码进入了 UMC Green CPU 的渲染函数,从而显着提高了该硬件的速度。 *