Анализ кода демо Second Reality

—

от автора

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). Вот невероятно полезные ссылки:

Часть 2: движок Second Reality

Как говорилось в части 1, основа Second Reality состоит из:

  • Загрузчика в виде исполняемого файла DOS.
  • Диспетчера памяти (простого пула стеков)
  • Сервера прерываний демо (Demo Interrupt Server, DIS).

В этой части я дам рекомендации программистам, которые захотят прочитать движок и загрузчик (DIS будет рассматриваться в следующей части).

Код движка

Код движка на 100% состоит из ASM, но он очень хорошо написан и довольно неплохо задокументирован:

В псевдокоде его можно записать так:

    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

Все этапы прочитать довольно просто:

  1. Установка сервера прерываний DIS как прерывания 0fch.
  2. Замена системных вызовов DOS по прерыванию 021h (подробнее об этом можно прочитать в части «Режимы Dev и Prod»).
  3. Загрузка музыки в звуковую карту через память EMS.
  4. Запуск музыки.
  5. Выполнение каждой части демо.
  6. Готово!

Подробности процедур 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 (обработчик прерываний, установленный на int 0fch).
  • 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 (и увидеть на схеме ниже):

  1. Таймер чипа 8254 настраивается на срабатывание IRQ0 с нужной частотой.
  2. Обработчик прерывания 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 загружался с помощью прерывания DOS 021h из отдельного файла.

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 можно было использовать любой язык:

Что касается использования памяти, то я много читал о MMU на Википедии и других веб-сайтах… но на самом деле, каждая часть могла использовать что угодно, потому что после выполнения она полностью выгружалась из памяти.

При работе с VGA каждая часть использовала собственный набор трюков и работала в своём разрешении. Во всех них использовались не Mode 13h и не ModeX, а скорее изменённый режим mode 13h с собственным разрешением. В файле SCRIPT часто упоминаются 320×200 и 320×400.

К сожалению, при анализе 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 пикселя, всё не так уж плохо.

Интересный факт: движок выполняет преобразования «олдскульным» способом: вместо использования общих однородных матриц 4×4 он использует матрицы поворота 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:

ссылка на оригинал статьи https://habr.com/ru/post/501030/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *