Зимой 2024 года я восстанавливал IBM PS/1 486-DX2 66Mhz, «Mini-Tower», model 2168. В подростковом возрасте я мечтал о таком компьютере, но не мог себе его позволить. Не могу выразить словами, насколько меня радовала работа над этой машиной.

Как только мне удалось его запустить, я сразу же провёл бенчмарк одного ПО.
C:DOOM>doom.exe -timedemo demo1
timed 1710 gametics in 2783 realtics
Doom не сообщает FPS напрямую. Для получения частоты кадров необходимо выполнить вычисления. В данном случае получилось 1710/2783*35 = 21,5 fps. Замечательная производительность для наилучшей машины, которую можно было купить за деньги (разумные) в декабре 1993 года (спецификация, чипсет, видео, disk1, disk2, speedsys).
Потом я услышал о fastDOOM. Обычно я не любитель портов, потому что в них часто добавляют разные несогласованные фичи (если не считать прекрасного Chocolate DOOM), но из любопытства я решил его попробовать.
C:DOOM>fdoom.exe -timedemo demo1
Timed 1710 gametics in 1988 realtics. FPS: 30.1
На 30% быстрее без вырезания из игры каких-либо фич[1]! На требовательной к ресурсам карте наподобие demo1 doom2 рост оказался ещё выше, с 16,8 fps до 24,9 fps. Игра стала на 48% быстрее!
Я не подозревал, что в DOOM оставалось так много возможностей для оптимизации. Очевидно, из-за того, что игру выпустили спустя год, на оптимизацию было мало времени. Я должен был понять, как провернули этот трюк.
Немного истории
Прежде чем разбираться с fastDOOM, давайте выясним, откуда взялся его код. Изначально DOOM разрабатывался на рабочей станции NeXT Workstation. Игру структурировали так, чтобы можно было легко портировать основную часть кода ядра, окружённого небольшими подсистемами, которые занимались вводом-выводом.

Источник: Game Engine Black Book: DOOM
В процессе разработки id Software писала ввод-вывод в DOS. Результат этой работы стал коммерческим релизом DOOM. Но выложить в опенсорс эту версию в 1997 году было нельзя, потому что в ней использовалась проприетарная звуковая библиотека DMX.
В опенсорс выложили версию для Linux, подчищенную Берндом Краймайзером при работе над книгой, объясняющей устройство движка.
DOS-версия DOOM была воссоздана на основе ядра из Linux-версии, ввода-вывода Heretic и APODMX (Apogee Sound wrapper), эмулирующего DMX. Так как в Heretic использовался видеорежим 13h, а в DOOM — видеорежим Y, графический ввод-вывод (i_ibm.c) был получен реверс-инжинирингом дизассемблированного кода DOOM.EXE. Именно так сообщество игроков получило PCDOOM v2[2].
Начальной точкой для fastDOOM стал PCDOOM v2.
┌───────────────┐
│ NeXTStep DOOM │
└─────┬────┬────┘
│ │
│ │
│ │
┌────────────┐ │ │ ┌──────┐ ┌─────────┐
│ Linux DOOM │◄─┘ └─►│ DOOM ├─────►│ Heretic │
└──────┬─────┘ └──────┘ └────┬────┘
│ ⁞ │
│ ▼ │
│ ┌──────────┐ │
└─────────────►│ PCDOOMv2 │◄────────┘
└─────┬────┘
▼
┌──────────┐
│ fastDOOM │
Генеалогия fastDoom └──────────┘
───────────────────
Общая картина производительности
Виктор "Viti95" Нието составил заметки о релизах, описывающие улучшения производительности каждой из версий, но, похоже, ему интереснее было сделать FDOOM.EXE крутым, а не описывать в подробностях, как он это сделал.
Чтобы представить общую картину эволюции производительности, я скачал все 52 релиза fastDOOM, PCDOOMv2 и оригинального DOOM.EXE, написал на Go программу для генерации RUN.BAT, запускающего -timedemo demo1 для всех этих версий, и смонтировал это всё при помощи mTCP NETDRIVE.
Я выбрал timedemo DOOM.WAD со звуком и размером экрана = 10 (полный экран с панелью статуса). За несколько часов стрельбы из дробовика и агонии импов я прогнал весь набор тестов пять раз и создал график усреднённого fps при помощи chart.js.

В оригинале статьи все графики интерактивны.
Первым делом этот график позволяет исключить гипотезу о том, что причиной улучшений fastDOOM стал современный компилятор. PCDOOMv2 собран при помощи OpenWatcom 2, но по сравнению с DOOM.EXE улучшения его незначительны.
Git-археология
Кроме частого выпуска релизов Viti95 продемонстрировал потрясающую дисциплину работы с git, где каждый коммит выполняет ровно одно действие и каждый релиз помечен тэгами. История git fastDOOM состоит из 3042 коммитов, что позволяет выполнить бенчмарк каждой фичи.
Я написал ещё одну программу на Go для сборки каждого коммита. Пропущу кровавые подробности работы с таким количеством изменений систем сборки (особенно от DOS к Linux). Спустя час была готова самая уродливая моя программа и 3042 файлов DOOM.EXE. Я с радостью отметил, что сборка почти ни разу не ломалась.

Составление графика размеров файлов показывает, что поначалу работа заключалась в подчистке и удалении кода. Большое снижение объёма кода произошло в bf0e983 (сборка 239, из которой была удалена запись звука), 5f38323 (сборка 0340, из которой удалены строки кодов ошибок) и 8b9cac5 (сборка 1105, в которой TASM заменён на NASM).
Идём глубже
Timedemo всех сборок заняло бы очень много времени (3042x1,5/60/24 * 3 прохода = 9 дней) поэтому я выбрал релиз, в котором был получен наибольший прирост скорости. Я написал ещё одну программу на Go для генерации файла .BAT, запускающего timedemo для всех коммитов в v0.1, v0.6, v0.8, v0.9.2 и v0.9.7. Я смонтировал 1,4 ГиБ разных FDOOM.EXE при помощи mTCP и запустил их. Это заняло много времени, потому что на версии со средой исполнения в двести с лишним коммитов тратилось 8 часов на каждый прогон.
FASTDOOM V0.1
В этом релизе представлено 220 коммитов.
$ git log --reverse --oneline "0.1" | wc -l
220

Переломным патчем для v0.1, без сомнения, стала сборка 36 (e16bab8). «Crispy-оптимизация» превращает рендеринг процентов панели состояния в noop, если они не поменялись. Это предотвращает рендеринг в scrap-буфер и вывод битов на экран, увеличивая частоту кадров на 2 fps. Поначалу я не мог в это поверить и подумал, что в моём тулчейне есть баг. Но включение этого патча в PCDOOMv2 действительно привело к огромному росту скорости.
Следующей идёт сборка 167 (a9359d5), встраивающая в код FixedDiv при помощи макроса.
Ближе к концу мы видим серию оптимизаций, обеспечивающую 0,5 fps.
Сборка 207 (9bd3f20): оптимизация PSX Doom, оптимизирующая способ обхода BSP.
Сборка 212 (dc0f48e): «встроенная 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) было несколько важных.
Сборка 342 (22819fd): пропуск рендеринга ненужных visplane.
Сборка 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-оптимизация.

Самые важные изменения:
Сборка 792 (f279b7d): по одному исполняемому файлу на рендерер (FDOOM.EXE, FDOOM13H.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

Самые важные изменения:
Сборка 1639 (ae2a951): оптимизация сравнения skyflatnum.
Сборка 1645 (0730cdc): оптимизация R_DrawColumn для режима Y.
Сборка 1646 (17c9e83): подчистка кода R_DrawSpan.
FASTDOOM V0.9.7
В этом релизе представлено 293 коммита.
$ git log --reverse --oneline "0.9.6"^.."0.9.7" | wc -l
294
Несмотря на многократные прогоны бенчмарка, мне не удалось снизить шум этого релиза.

Самые важные изменения:
Сборка 1941 (0688235): тестирование изменений в ASM x86.
Сборка 1943 (f326e73): добавлен выбор CPU + оптимизация CR2 для 386SX.
Сборка 1944 (a836abb): добавлена оптимизация ESP для R_DrawSpan386SX.
Сборка 2000 (3432590): добавлен базовый код для рендеринга нечётких столбцов на ASM.
Сборка 2031 (0edab46): удаление сравнения CMP в каждом цикле (оптимизация Кена Сильвермана?).
MODE 13H и MODE Y
Разработчик fastDOOM исследовал множество разных способов ускорения работы для широкого спектра процессоров (386, 486, Pentium, Cyrix) и видеошин (ISA, VLB, PCI). На моей машине не заработала оптимизация, позволяющая использовать видеорежим 13h вместо режима Y.
В режиме 13h распределение данных по четырём банкам VRAM карты VGA выполняется аппаратно. Для процессора память VRAM выглядит как единый линейный буфер кадров 320x200. Неудобство заключается в том, что нельзя реализовать двойной буфер во VRAM, поэтому это необходимо делать в RAM, из-за чего байты записываются дважды: сначала в буфер кадров в RAM, а затем во второй раз при отправке во VRAM. Кроме того, движок должен использовать VSYNC.
Mode 13h
──────── RAM VRAM (VGA-карта) ЭКРАН
┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐
│ ┌───────────────┐ │ │ │ │ │
│ │ framebuffer 1 │ │ │ │ │ │
│ └───────────────┘ │ │ │ │ │
│ ┌───────────────┐ │ │ ┌───────────────┐ │ │ │
CPU ────►│ │ framebuffer 2 │ ├────► │ │framebuffer(fb)│ ├──────►│ │
│ └───────────────┘ │ │ └───────────────┘ │ │ │
│ ┌───────────────┐ │ │ │ │ │
│ │ framebuffer 3 │ │ │ │ │ │
│ └───────────────┘ │ │ │ │ │
└───────────────────┘ └───────────────────┘ └───────────────────┘
Режим Y позволяет программистам получать доступ к банкам VGA по отдельности. Благодаря этому возможна тройная буферизация во VRAM. Кроме того, данные можно записывать только один раз, непосредственно во VRAM. Целевой банк вручную выбирается разработчиком при помощи очень медленных команд OUT, зато это позволяет дублировать пиксели по горизонтали (таким образом мы без траты ресурсов получаем режим низкой детализации), выполняя запись в два банка VGA одновременно при помощи регистров-защёлок[3]. Ещё одно неудобство заключается в том, что это сильно замедляет отрисовку невидимого монстра Specter, потому что она требует обратного считывания из VRAM.
Режим Y
─────── VRAM (VGA-карта) ЭКРАН
┌───────────────────┐ ┌───────────────────┐
│ ┌───────────────┐ │ │ │
│ │fb1 | fb2 | fb3│ │ │ │
│ └───────────────┘ │ │ │
│ ┌───────────────┐ │ │ │
│ │fb1 | fb2 | fb3│ │ │ │
CPU ──────────────────────────────► │ └───────────────┘ ├──────►│ │
│ ┌───────────────┐ │ │ │
│ │fb1 | fb2 | fb3│ │ │ │
│ └───────────────┘ │ │ │
│ ┌───────────────┐ │ │ │
│ │fb1 | fb2 | fb3│ │ │ │
│ └───────────────┘ │ │ │
└───────────────────┘ └───────────────────┘
Для машин с быстрыми CPU и шиной (от 100 МГц/Pentium и VLB/PCI), видеокарты которых, скорее всего, плохо обрабатывают команду OUT, больше подходит режим 13h. Для «медленных CPU» быстрее записывать данные один раз во VRAM в режиме Y.
Как бы то ни было, в Doom использовался режим Y.
В DOOM использовался VGA-режим 320*200*256, который слегка отличается от режима MCGA (он НЕ заработает на машине с MCGA). Я выполняю доступ к буферу кадров в планарном режиме с чередованием, аналогично «Mode X» Майкла Абраша, но всё равно использую 200 растровых линий вместо 240 (меньше пикселей == повышенная частота обновления).
DOOM циклически переключается между тремя отображаемыми страницами. Если бы использовались только две, то приходилось бы синхронизироваться с VBL, чтобы избежать возможного мерцания экрана. Если внимательно изучить эффект HOM, то можно заметить циклическое переключение между тремя изображениями.
- Джон Кармак[4] (зеркало)
Ещё одна причина того, что Джон выбрал для игры режим Y, заключается в том, что инструменты команды художников (Deluxe Paint) поддерживали только 320x200 (а Mode-X имел разрешение 320x240).
e...@agora.rdrop.com (Ed Hurtley) написал: >Проверьте, пожалуйста... Если ни разу не нажимать на ESC, в меню Options
> можно переключать разрешение Low/High... Low — это 320x200, High —
> это 640x400, а графика рамки (панель, меню и так далее...)
>всё равно остаются в 320x200... (Просто те же графические файлы)
Низкая детализация — это 160*200 в окне. Она реализована установкой двух битов в регистре mapmask, когда функции текстурирования выполняют запись в видеопамять, благодаря чему на каждый записанный байт задаётся два пикселя.
ui...@freenet.Victoria.BC.CA (Ben Morris) написал:
>Джон,
>Вы используете систему планарной графики в растровой игре,
>обновляющей весь экран с хорошей частотой кадров на 486/66?
Она планарная, но не битово-планарная (это было бы ужасно). Пиксели 0,4,8 находятся на плоскости 0, пиксели 1,5,9 — на плоскости 1 и так далее.
>Это невероятно. Я бы подумал, что из-за лишней траты ресурсов
>при программировании регистров VGA это было бы
>невозможно.
Регистры не нужно особо программировать. Регистр mapmask нужно задавать только один раз для каждого вертикального столбца и по четыре раза для каждой горизонтальной строки (я двигаюсь во внутреннем цикле с шагом в четыре пикселя, чтобы оставаться на одной плоскости, а затем выполняю инкремент начального пикселя и перехожу к следующей плоскости).
Это всё равно довольно жульническое решение, достаточно сильно загрязняющее программу, но наложение текстур непосредственно в видеопамяти прилично увеличивает скорость (на 10-15%) с большинством видеокарт, потому что операции записи видео перемежаются с операциями доступа к основной памяти и вычислениями текстур, давая операции записи завершиться без простоев.
Эти мучения к тому же позволяют выполнять идеальное переключение страниц, а не с такими разрывами, которые возникают при буферизации в основной памяти.
- Джон Кармак[5] (зеркало)
Heretic был выпущен в 1994 году. Компьютеры эволюционировали, сделав режим 13h[6][7] привлекательнее, поэтому Raven с учётом этого изменила движок DOOM. В PCDoom v2 использовался ввод-вывод Heretic, но ввод-вывод видео был заново реализовал с режимом Y. fastDOOM даёт пользователям на выбор несколько исполняемых файлов: FDOOM.EXE, FDOOM13H.EXE и FDOOMVBD.EXE.
В пресс-релизной бете DOOM (октябрь 93 года) использовался режим 13h, поэтому предположу, что они перешли на режим Y, чтобы повысить производительность на медленных машинах (в режиме низкой детализации). Любопытно, почему они не реализовали так называемый «картофельный режим», записывающий во VRAM четыре пикселя за одну 8-битную операцию записи.
В FastDoom я вернул режим 13h, потому что для этого режима Heretic/Hexen имел более оптимизированный код рендеринга на ASM. Позже мне удалось частично портировать этот подход в рендеринг столбцов в режиме Y, что привело к росту производительности на 5-7%.
Судя по моим тестам, лучше всего для процессоров 486 подходит VESA direct mode (FDOOMVBD.EXE для 320x200). Этот режим сочетает в себе преимущества режима Y с оптимизированным кодом рендеринга из Heretic, при этом избегая использования команд OUT, за исключением команды для смены буферов, выполняемой только раз за отрендеренный кадр. Единственный его недостаток заключается в том, что для него нужна графическая карта VLB или PCI со включенным LFB, а в режимах низкой детализации и «картошки» производительность ниже.
- Из бесед с Viti95
В процессе вычитки статьи Viti95 подробнее рассказал о режиме 13h порта fastDOOM.
В FastDoom режим 13h использует единый буфер кадров в RAM, который копируется во VRAM после рендеринга всей сцены. Vsync не включается принудительно, что может привести к мерцанию. Существует два способа копирования заднего буфера во VRAM, оптимизированные под разные скорости шин. Для медленных шин (8-битных ISA), используется дифференциальное копирование, передающее только изменившиеся пиксели.
При этом способе используется много ветвлений, но в целом он быстрее, потому что ветвления менее затратны, чем избыточная передача данных по шине. В случае быстрых шин (16-битных ISA, VLB, PCI и так далее) выполняется полное копирование заднего буфера при помощи команд REP MOVS, которое эффективно при достаточно большой ширине шины.
- Из бесед с Viti95
Другие оптимизации, которые не сработали
Ещё один путь, исследование которого меня порадовало — это флаги конкретных процессоров в OpenWatcom (4r/4s и 3r/3s)[8]. Разработчик попробовал флаги wcc386 386 и 486, но в конечном итоге от них отказался, потому что версия 386 всегда была быстрее.
Одна из моих целей в FastDoom — сменить компилятор с OpenWatcom v2 на DJGPP (GCC), который из одних и тех же исходников генерирует более быстрый код. Было бы также здорово, если бы кто-то смог улучшить OpenWatcom v2, чтобы уничтожить этот разрыв в производительности.
- Из общения с Viti95
Общее впечатление
Виктор Нието проделал замечательную работу! Программа может умереть от тысячи порезов, а Viti95 улучшил fastDOOM при помощи трёх тысяч оптимизаций! Он не только использовал уже существовавшие улучшения (crispy, psx, gba, Lee Killough), но и придумал множество новых, создав такой ажиотаж, что к нему присоединился[9] даже Кен Сильверман (автор движка Build, на котором создан Duke3D).
Снимаю шляпу, Виктор!
Ссылки
[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
[7] Doom in DOS: Original vs Source Ports
[9] Примечание Viti95: некоторые из идей и часть кода Кена Сильвермана добрались до функций рендеринга UMC Green CPU, что существенно повысило скорость этого оборудования.
Автор: PatientZero