Quake: Как ручная оптимизация на ассемблере спасла производительность игры
В 1999 году, когда id Software опубликовала исходный код Quake, в файле readme.txt Джон Кармак оставил любопытное замечание. Он указал, что для сборки проекта необходим Masm, и хотя можно обойтись чистым C, версия с программным рендерингом в этом случае потеряет почти 50% производительности.
# Для компиляции ассемблерных файлов требуется Masm.
# Можно переключить #define на использование только C-кода,
# но скорость программного рендеринга упадет почти вдвое.
Действительно ли Quake обязан своей скоростью именно низкоуровневому коду? Давайте разберем, как это реализовано и какие архитектурные хитрости позволили добиться такого прироста.
Замеры производительности: ASM против C
Для объективности эксперимента тесты проводились на аутентичном железе — Pentium MMX 233 МГц. Первым делом нужно было определить базовую частоту кадров оригинальной версии winquake.exe.
C:\winquake> winquake.exe -wavonly +d_subdiv16 0 +timedemo demo1
Параметр d_subdiv16 был отключен, так как у него нет реализации на языке C, что сделало бы сравнение некорректным. Движок перешел на использование D_DrawSpans8 (перспективная выборка каждые 8 пикселей). В итоге оригинальный билд выдал в среднем 42,3 FPS.
Сборка без оптимизаций
Следуя инструкциям Кармака, я изменил значение id386 на 0 в файле quakedef.h. После решения проблем с линковщиком (путем добавления заглушек из nointel.c) был получен исполняемый файл на чистом C.
C:\winquake> WinQuake_No_ASM.exe -wavonly +d_subdiv16 0 +timedemo demo1
Результат подтвердил слова Кармака: частота кадров рухнула до 22,7 FPS. Без ассемблерных вставок Майкла Абраша игра стала работать почти в два раза медленнее.
Анатомия ассемблерного кода Quake
В исходниках Quake обнаружилось 63 функции на ассемблере, распределенные по 21 файлу. Это значительно больше, чем в DOOM, где за ускорение отвечали всего две функции.
Основные оптимизации сосредоточены в задачах рендеринга (префикс R_) и растеризации (префикс D_). Замеры показали, какой вклад вносит каждая группа функций:
| Функция | Прирост (FPS) |
|---|---|
| D_DrawSpans8 (отрисовка стен) | +12,6 |
| R_DrawSurfaceBlock8_mip* (кэширование поверхностей) | +4,2 |
| D_Polyset* (отрисовка моделей) | +2,2 |
| Прочие функции | +0,5 |
Ключевой секрет успеха заключался в глубоком понимании архитектуры процессора Pentium (P5). Майкл Абраш использовал развертку циклов, самомодифицирующийся код и параллельную загрузку конвейеров U и V.
Оптимизация математики: TransformVector
Функция TransformVector отвечает за проецирование координат. В стандартной реализации на C, сгенерированной компилятором VC6, инструкции FPU выполняются последовательно: умножение, ожидание, сложение. Это создает простои, так как fmul требует три такта.
Абраш переписал этот блок, создав очередь из независимых команд. Благодаря тому, что на Pentium команда обмена регистрами fxch выполняется за 0 тактов, он превратил стек FPU в массив регистров, вычисляя три скалярных произведения параллельно. Это позволило полностью скрыть задержки операций с плавающей запятой.
Генерация поверхностей: R_DrawSurfaceBlock8
Эта функция «запекает» карту освещения в текстуру. Здесь применен агрессивный подход с самомодифицирующимся кодом (SMC). Значения цветовой карты вшиваются прямо в поток инструкций перед вызовом функции, что освобождает регистры и избавляет от лишних операций сложения при поиске цвета.
Внутренний цикл полностью развернут, что убирает накладные расходы на счетчики и позволяет избежать ошибок предсказания переходов. Майклу Абрашу удалось довести скорость этого кода до 2,25 такта на тексел.
Растеризация и деление: D_DrawSpans8
Самая сложная часть — перспективно-корректное текстурирование. Для него требуется деление (1/z), которое на Pentium занимает до 39 тактов. Абраш применил «пересечение» (interleaving): пока FPU занят длительным делением для следующего блока из 8 пикселей, целочисленные конвейеры процессора успевают отрисовать текущие 8 пикселей.
Дополнительно используются таблицы переходов, исключающие ветвление в конце интервалов, и беззнаковое сравнение для одновременной проверки границ (clamping), что минимизирует вероятность ошибок предсказания перехода.
Заключение
Успех Quake в эпоху программного рендеринга — это результат ювелирной работы с архитектурой x86. Майкл Абраш и Джон Кармак не просто писали код, они буквально «дирижировали» конвейерами процессора, заставляя их работать на пределе возможностей, что и обеспечило те самые заветные 40 FPS на «сотках» Pentium.
Для детального изучения можно ознакомиться с главой 68 «Quake’s Lighting Model» в книге Майкла Абраша Graphic Programming Black Book.

