23 июля 2013 года был опубликован исходный код демо Second Reality (1993 год). Как и многим, мне не терпелось взглянуть на внутренности демо, которое так вдохновляло нас на протяжении всех этих лет.
Я ожидал увидеть монолитный хаос из ассемблера, но вместо него я, к удивлению своему, обнаружил сложную архитектуру, изящным образом объединяющую несколько языков. Никогда раньше не видел я подобного кода, идеально представляющего два неотъемлемых аспекта разработки демо:
- Командная работа.
- Обфускация.
Как обычно, свои заметки я сформировал статью: надеюсь, кому-нибудь это сэкономит несколько часов, и, возможно, вдохновит остальных читать больше исходного кода и становиться более опытными инженерами.
Часть 1: введение
Демо
Прежде чем приступать к коду, дам ссылку на захват легендарного демо в HD-видео (Майкла Хата). Сегодня это единственный способ полноценно оценить демо без графических глитчей (даже DOSBox не может правильно его запускать).
Первый контакт с кодом
Исходный код выложен на GitHub. Достаточно ввести одну команду
git
: git clone git@github.com:mtuomi/SecondReality.git
Поначалу содержимое сбивает с толку: 32 папки и загадочный
U2.EXE
, который не запускается в DosBox.Демо имело рабочее название «Unreal 2» (первым «Unreal» стало предыдущее демо Future Crew, выпущенное для первой Assembly в 1992 году). И только в процессе разработки название сменили на «Second Reality». Это объясняет имя файла «U2.EXE», но не почему файл не работает…
Если запустить CLOC, то мы получим интересные метрики:
------------------------------------------------------------------------------- Язык файлы пробелы комментарии код ------------------------------------------------------------------------------- Assembly 99 3029 1947 33350 C++ 121 1977 915 24551 C/C++ Header 8 86 240 654 make 17 159 25 294 DOS Batch 71 3 1 253 ------------------------------------------------------------------------------- SUM: 316 5254 3128 59102 -------------------------------------------------------------------------------
- Кодовая база состоит «всего» из 50% ассемблера.
- Кодовая база почти вдвое больше движка Doom.
- В ней есть семнадцать makefile. Почему не всего один?
Запуск демо
В этом сложно разобраться, но выпущенное демо можно запустить в DosBox: надо переименовать
U2.EXE
и запустить его из нужного места.Когда я узнал о внутренней работе кода, это стало выглядеть очень логично:
CD MAIN MOVE U2.EXE DATA/SECOND.EXE CD DATA SECOND.EXE
И вуаля!
Архитектура
В 90-х демо в основном распространялись да гибких дисках. После распаковки нужно было установить два больших файла:
SECOND.EXE
и REALITY.FC
:. <DIR> 01-08-2013 16:40 .. <DIR> 01-08-2013 16:40 FCINFO10 TXT 48,462 04-10-1993 11:48 FILE_ID DIZ 378 04-10-1993 11:30 README 1ST 4,222 04-10-1993 12:59 REALITY FC 992,188 07-10-1993 12:59 SECOND EXE 1,451,093 07-10-1993 13:35 5 Files(s) 2.496,343 Bytes. 2 Dir(s) 262,111,744 Bytes free.
Исходя из своего опыта разработки игр, я всегда ожидаю, что картина в целом должна выглядеть так:
SECOND.EXE
: движок со всеми эффектами в исполняемом файле.REALITY.FC
: ассеты (музыка, звуковые эффекты, изображения) в проприетарном/зашифрованном формате а-ляWAD
игры Doom.
Но после прочтения
MAIN/PACK.C
я обнаружил, что сильно ошибался: движок «Second Reality» — это всего лишь загрузчик (Loader) и сервер прерываний (Interrupt server) (который называется DIS). Каждая сцена (называемая также «PART») демо — это полнофункциональный исполняемый файл DOS. Каждая part (часть) загружается загрузчиком Loader и запускается одна за другой. Части хранятся в зашифрованном виде в конце SECOND.EXE
:REALITY.FC
содержит две музыкальные композиции, воспроизводимые во время демо (для обфускации добавлены заполнение и маркер в начале).SECOND.EXE
содержит загрузчик и Demo Interrupt Server (DIS).- После конца
SECOND.EXE
добавлены 32 частей (PART) демо в виде исполняемых файлов DOS (зашифрованных).
Такая архитектура обеспечивает множество преимуществ:
- Более удобная совместная работа в команде: каждый участник команды может отдельно работать над своей PART при условии, что создаст исполняемый файл с символом
_start
и останется в пределах ограничений памяти (450 КБ). - Зашифровка файлов EXE в конец
SECOND.EXE
и их загрузка во время выполнения усложняют реверс-инжиниринг. - Быстрый запуск: Loader и DIS имеют размер всего 20 КБ. DOS загружает их очень быстро.
- Простое управление памятью: после завершения PART загрузчик заменяет её следующей PART и освобождает ВСЮ выделенную память.
- Нет необходимости в менеджере/загрузчике ассетов: как мы увидим позже, каждая PART содержит в себе свои ассеты (в основном изображения), скомпилированные в коде: при загрузке EXE также загружаются все необходимые изображения, как в удобном пакете.
- Для программирования PART можно использовать любой язык: в коде мы находим C, Assembly… и Pascal.
Рекомендуемое чтение
Три столпа для понимания исходного кода Second Reality — это VGA, ассемблер и архитектура PC (программирование PIC и PIT). Вот невероятно полезные ссылки:
- Архитектура VGA: FreeVGA Project
- The Art of Assembly (особенно глава 17 о прерываниях и ISR).
- Программирование PIT 8254
- Так как в исходном коде полно указателей «FAR» и «NEAR», будет полезно прочитать Basic Architecture Documentation компании Intel, и в особенности главу 4.3 о типах данных указателей.
- Всеобъемлющие статьи Майкла Абраша из Graphic Programming Black Book: глава 3: (Zen Timer and PIT), глава 24: VGA tour, главы 47,48 и 49: (Mode X).
Часть 2: движок Second Reality
Как говорилось в части 1, основа Second Reality состоит из:
- Загрузчика в виде исполняемого файла DOS.
- Диспетчера памяти (простого пула стеков)
- Сервера прерываний демо (Demo Interrupt Server, DIS).
В этой части я дам рекомендации программистам, которые захотят прочитать движок и загрузчик (DIS будет рассматриваться в следующей части).
Код движка
Код движка на 100% состоит из ASM, но он очень хорошо написан и довольно неплохо задокументирован:
MAIN/U2.ASM
(ядро).MAIN/U2A.ASM
(утилиты).MAIN/VMODE.ASM
(процедуры VGA).
В псевдокоде его можно записать так:
exemus db 'STARTMUS.EXE',0
exe0 db 'START.EXE',0
...
exe23 db 'ENDSCRL.EXE',0
start:
cli ; Disable all interrupts
mov ah,4ah ; Deallocate all memory
call checkall ; Check for 570,000 bytes of mem, 386 CPU and VGA
call file_getexepath
call dis_setint ; Install Demo Interrupt Server on Interrupt 0fch
call file_initpacking ; Check exe signature (no tempering) !
call file_setint ; Replace DOS routines (only OPENFILE, SEEK and READ) on Interrupt 021h
call flushkbd ; Flush the keyboard buffer
call checkcmdline ; check/process commandline
;======== Here we go! ========
call vmode_init ; Init VGA (not necessarly Mode13h or ModeX), each PARTs had its own resolution
mov si,OFFSET exe0
call executehigh ; loaded to high in memory. Used for loading music loaders and stuff.
call _zinit ; Start music
call restartmus
mov si,OFFSET exe1 ;Parameter for partexecute: Offset to exec name
call partexecute
; Execute all parts until exe23
call fademusic
;======== And Done! (or fatal exit) ========
fatalexit:
mov cs:notextmode,0
call vmode_deinit
Все этапы прочитать довольно просто:
- Установка сервера прерываний DIS как прерывания
0fch
. - Замена системных вызовов DOS по прерыванию
021h
(подробнее об этом можно прочитать в части «Режимы Dev и Prod»). - Загрузка музыки в звуковую карту через память EMS.
- Запуск музыки.
- Выполнение каждой части демо.
- Готово!
Подробности процедур
execute
: execute:
cld
call openfile ; Open the DOS executable for this PART
call loadexe ; loads the specified exe file to memory, does relocations and creates psp
call closefile
call runexe ;runs the exe file loaded previously with loadexe.
; returns after exe executed, and frees the memory
; it uses.
Диспетчер памяти
Было много легенд о том, что Second Reality использует сложный диспетчер памяти через MMU, в движке его следов не нашлось. Управление памятью на самом деле передано DOS: движок начинает работу с освобождения всей ОЗУ с последующим распределением её по запросу. Единственный хитрый трюк заключается в возможности выделения ОЗУ из конца кучи: оно выполняется при помощи возвращаемого значения malloc DOS, когда запрашивается слишком много ОЗУ.
Часть 3: DIS
Demo Interrupt Server (DIS) предоставляет широкий набор услуг каждой из PART: от обмена данными между разными PART до синхронизации с VGA.
Услуги DIS
Во время выполнения PART сервер DIS предоставляет ей услуги. Список функций можно найти в
DIS/DIS.H
.Наиболее важные услуги:
- Обмен данными между разными PART (
dis_msgarea
): DIS предоставляет три буфера по 64 байт, чтобы PART могла получать параметры из загрузчика предыдущей PART. - Эмуляция Copper (
dis_setcopper
): симулятор Amiga Copper, позволяющий выполнять операции, переключаемые состоянием VGA. - Режим Dev/Prod (
dis_indemo
): позволяет PART узнать, что она запущена в режиме DEV (а значит, должна выполнять инициализацию видео) или запущена из загрузчика в режиме PROD. - Подсчёт кадров VGA (
_dis_getmframe
) - Ожидание обратного хода луча VGA (
dis_waitb
).
Код Demo Interrupt Server
Исходный код DIS тоже на 100% состоит из ASM… и довольно неплохо прокомментирован:
DIS/DIS.ASM
(обработчик прерываний, установленный на int0fch
).DIS/DISINT.ASM
(сами процедуры DIS).- Так как Second Reality частично написана и на C, в коде есть интерфейс для C:
DIS/DIS.H
иDIS/DISC.ASM
.
Как это работает
DIS устанавливается как обработчик прерываний для программного int
0fch
. Здорово здесь то, что он может запускаться внутри SECOND.EXE
, когда работает демо, или как резидентная программа (TSR) в режиме Dev. Такая гибкость позволяет по отдельности тестировать различные PART демо во время разработки:// Let's pretend we are a FC developer and want to start the STAR part directly. C:\>CD DDSTARS C:\DDSTARS>K ERROR: DIS not loaded. // Oops, the PART could not find the DIS at int 0fch. C:\DDSTARS>CD ..\DIS C:\DIS>DIS Demo Int Server (DIS) V1.0 Copyright (C) 1993 The Future Crew BETA VERSION - Compiled: 07/26/93 03:15:53 Installed (int fc). NOTE: This DIS server doesn't support copper or music synchronization! // DIS is installed, let's try again. C:\DIS>CD ../DDSTARS C:\DDSTARS>K
И вуаля!
Copper
«Copper» — это сопроцессор, который любили разработчики демо для Amiga. Он являлся частью Original Chip Set и позволял выполнять программируемый поток команд, синхронизированный с видеооборудованием. На PC не было такого сопроцессора, и Future Crew пришлось написать симулятор Copper, работающий внутри DIS.
Для симуляции Copper команда FC использовала чипсет 8254-PIT и 8259-PIC оборудования PC. Она создала систему, синхронизированную с частотой VGA, способную запускать процедуры в трёх местах вертикального обратного хода луча:
- Место 0: после включения дисплея (примерно на строке развёртки 25)
- Место 1: сразу после обратного хода луча развёртки (ПО ВОЗМОЖНОСТИ СТОИТ ЭТОГО ИЗБЕГАТЬ)
- Место 2: в обратном ходе луча развёртки
О том, как это сделано, можно прочитать в
MAIN/COPPER.ASM
(и увидеть на схеме ниже):- Таймер чипа 8254 настраивается на срабатывание IRQ0 с нужной частотой.
- Обработчик прерывания 8h (который вызывается 8259 PIC после получения IRQ0) здесь заменяется на процедуру
intti8
.
Примечание: услуга подсчёта кадров DIS на самом деле предоставляется симулятором copper.
Часть 4: режимы Dev и Prod
Читая исходный код Second Reality, больше всего поражаешься тому, сколько внимания команда уделила беспроблемному переключению из DEV в PROD.
Режим Development
В режиме Development каждый компонент демо являлся отдельным исполняемым файлом.
- DIS загружался в резидентную TSR и доступ к нему осуществлялся через прерывание
0cfh
. - Загрузчик вызывал прерывание DOS
21h
для открытия, чтения, поиска и закрытия файлов.
Такая конфигурация DEV имеет следующие преимущества:
- Каждый кодер и художник мог работать над исполняемым файлом и тестировать его отдельно, не влияя на остальную часть команды.
- Полное демо в любой момент можно было протестировать при помощи небольшого
SECOND.EXE
(без добавления всех EXE в конец). Исполняемый файл каждой PART загружался с помощью прерывания DOS021h
из отдельного файла.
Production (режим демо)
В режиме Production небольшой
SECOND.EXE
(содержащий загрузчик), DIS и части демо в виде отдельных EXE объединялись в один толстый SECOND.EXE
.- Доступ к DIS по-прежнему выполнялся через прерывание
0fch
. - API прерывания DOS 21h был пропатчен собственными процедурами Future Crew, которые открывают файлы из конца большого файла
SECOND.EXE
.
Такая конфигурация PROD имеет преимущество с точки зрения времени загрузки и защиты от реверс-инжиниринга… но самое важное — с точки зрения программирования или загрузки PART, при переходе с DEV на PROD не меняется НИЧЕГО.
Часть 5: отдельные PART
Каждый из визуальных эффектов Second Reality является полнофункциональным исполняемым файлом DOS. Они называются PART и всего их 23. Такое архитектурное решение позволило обеспечить быстрое прототипирование, параллельную разработку (поскольку у FC, скорее всего, не было инструментов контроля версий) и свободный выбор языков (в исходниках встречаются ASM, C и даже Pascal).
Отдельные PART
Список всех PART/EXE можно найти в исходном коде движка: U2.ASM. Вот более удобное краткое описание всех 23 частей (с расположением исходного кода, хотя названия могут сильно сбивать с толку):
Название | Исполняемый файл | Кодер | Скриншот | Исходный код |
STARTMUS.EXE | MAIN/STARTMUS.C | |||
START.EXE | WILDFIRE | START/MAIN.c | ||
Hidden part | DDSTARS.EXE | WILDFIRE | DDSTARS/STARS.ASM | |
Alkutekstit I | ALKU.EXE | WILDFIRE | ALKU/MAIN.C | |
Alkutekstit II | U2A.EXE | PSI | VISU/C/CPLAY.C | |
Alkutekstit III | PAM.EXE | TRUG/WILDFIRE | PAM/ | |
BEGLOGO.EXE | BEG/BEG.C | |||
Glenz | GLENZ.EXE | PSI | GLENZ/ | |
Dottitunneli | TUNNELI.EXE | TRUG | TUNNELI/TUN10.PAS | |
Techno | TECHNO.EXE | PSI | TECHNO/KOEA.ASM | |
Panicfake | PANICEND.EXE | PSI | PANIC | |
Vuori-Scrolli | MNTSCRL.EXE | FOREST/READ2.PAS | ||
Desert Dream Stars | DDSTARS.EXE | TRUG | ||
Lens | PSI | |||
Rotazoomer | LNS&ZOOM.EXE | PSI | LENS/ | |
Plasma | WILDFIRE | |||
Plasmacube | PLZPART.EXE | WILDFIRE | PLZPART/ | |
MiniVectorBalls | MINVBALL.EXE | PSI | DOTS/ | |
Peilipalloscroll | RAYSCRL.EXE | TRUG | WATER/DEMO.PAS | |
3D-Sinusfield | 3DSINFLD.EXE | PSI | COMAN/DOLOOP.C | |
Jellypic | JPLOGO.EXE | PSI | JPLOGO/JP.C | |
Vector Part II' | U2E.EXE | PSI | VISU/C/CPLAY.C | |
Титры/благодарности | ||||
ENDLOGO.EXE | END/END.C | |||
CRED.EXE | WILDFIRE | CREDITS/MAIN.C | ||
ENDSCRL.EXE | ENDSCRL/MAIN.C |
Похоже, у каждого разработчика была своя специализация, которые могли совместно использоваться в одной части. Особенно это заметно в первой сцене со скроллингом, кораблями и взрывами (Alkutekstit). Хотя это выглядит как один непрерывный эффект, на самом деле это три исполняемых файла, написанных тремя разными людьми:
Alkutekstit (Credits) sequence | ||
ALKU by WILDFIRE | U2A by PSI | PAM by TRUG/WILDFIRE |
Ассеты
Ассеты изображений (
.LBM
) сгенерированы с помощью Deluxe Paint — чрезвычайно популярного в 90-х редактора битовых карт. Интересно, что они преобразованы в массив байтов и скомпилированы внутри PART. В результате этого файл exe также загружает все ассеты. Кроме того, это усложняет реверс-инжиниринг.Среди крутых наборов ассетов можно назвать знаменитый CITY и SHIP из последней 3D-сцены:
Внутреннее устройство PART
Так как все они скомпилированы в исполняемые файлы DOS, в PART можно было использовать любой язык:
- ASM.
- C.
- PASCAL: Brass knuckles monster (MNTSCRL.EXE), Dotted Tunnel (TUNNELI.EXE) и Raytracing sword (RAYSCRL.EXE).
- Некоторые PART зашли ещё дальше и используют C для генерации кода на ASM: GLENZ/DOLOOP.C (скачущий многогранник) и Water effect (3DSINFLD.EXE).
Что касается использования памяти, то я много читал о MMU на Википедии и других веб-сайтах… но на самом деле, каждая часть могла использовать что угодно, потому что после выполнения она полностью выгружалась из памяти.
При работе с VGA каждая часть использовала собственный набор трюков и работала в своём разрешении. Во всех них использовались не Mode 13h и не ModeX, а скорее изменённый режим mode 13h с собственным разрешением. В файле SCRIPT часто упоминаются 320x200 и 320x400.
К сожалению, при анализе PART чтение исходного кода становится сложной задачей: качество кода и комментариев сильно падает. Возможно, так произошло из-за спешки или из-за того, что над каждой PART работал свой разработчик (то есть «реальной» необходимости в комментариях или понятности кода не было), но в результате получилось нечто совершенно запутанное:
Сложны не только алгоритмы, трудно разбираться даже с именами переменных (
a
, b
, co[]
,… ). Код был бы намного более читаемым, если бы разработчики оставили нам подсказки в примечаниях к релизу. В результате я не уделял изучению каждой части особо много времени; исключение составил 3D-движок, отвечающий за U2A.EXE и U2E.EXE.3D-движок Second Reality
Я всё равно решил подробно изучить 3D-движок, который использовался в двух частях:
U2A.EXE
и U2E.EXE
.Исходный код представляет собой C с оптимизированными ассемблером процедурами (особенно заливка и затенение по Гуро):
- CITY.C (основной код).
- VISU.C (библиотека
visu.lib
). - AVID.ASM (оптимизированный ассемблер видео (очистка, копирование экрана и т.п.)).
- ADRAW.ASM (отрисовка объектов и усечение).
- ACALC.ASM (матрицы и быстрые вычисления sin/cos).
Архитектура этих компонентов довольно примечательна: библиотека
VISU
выполняет все сложные задачи, например, загрузку ассетов: 3DS-объектов, материалов и потоков (движений камеры и кораблей).Движок сортирует объекты, которые нужно отрисовывать, и рендерит их при помощи алгоритма художника. Это приводит к большому объёму перерисовки, но поскольку защёлки VGA позволяют одновременно записывать 4 пикселя, всё не так уж плохо.
Интересный факт: движок выполняет преобразования «олдскульным» способом: вместо использования общих однородных матриц 4x4 он использует матрицы поворота 3*3 и вектор перемещения.
Вот краткое изложение в псевдокоде:
main(){
scenem=readfile(tmpname); // Load materials
scene0=readfile(tmpname); // Load animation
for(f=-1,c=1;c<d;c++){ //Load objects
sprintf(tmpname,"%s.%03i",scene,e);
co[c].o=vis_loadobject(tmpname);
}
vid_init(1);
vid_setpal(cp);
for(;;){
vid_switch();
_asm mov bx,1 _asm int 0fch // waitb for retrace via copper simulator interrupt call
vid_clear();
// parse animation stream, update objects
for(;;){}
vid_cameraangle(fov); // Field of vision
// Calc matrices and add to order list (only enabled objects)
for(a=1;ac<conum;a++) if(co[a].on) /* start at 1 to skip camera */
calc_applyrmatrix(o->r,&cam);
// Zsort via Bubble Sort
for(a=0;ac<ordernum;a++)
for(b=a-1;b>=0 && dis>co[order[b]].dist;b--)
// Draw
for(a=0;ac<ordernum;a++)
vis_drawobject(o);
}
}
return(0);
}
Порты на современные системы
После выхода этой статьи многие разработчики начали портировать Second Reality на современные системы. Клаудио Мацуока приступил к созданию sr-port — порта на C для Linux и OpenGL ES 2.0, который пока выглядит довольно впечатляюще. Ник Ковач проделал большую работу над PART PLZ, портировав её на C (теперь она является частью исходного кода sr-port), а также на javascript: