История о том, как Майкл Абраш в два раза ускорил Quake

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.

 

Источник

Читайте также