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





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 часто упоминаются 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:




Комментарии (6):

  1. romwhite
    /#21594862 / +2

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

    • br0x
      /#21604128

      В 93/94 годах этим и занимался, распаковал все до состояния, описанного в этой статье, а самые интересные эффекты декомпилировал.
      Быть сторожем не главное, достаточно быть молодым и бескрышным)

      • romwhite
        /#21605688

        Прикольно. Завидую. Я в 94-м в кружке железнодорожного моделирования ждал в очереди, чтобы поиграть на Поиске в какой-нибудь примитив.

  2. ICELedyanoj
    /#21595372 / +2

    Спасибо за статью! Наконец-то смог найти демку, которую продавец первого компа записал мне на винчестер.
    Нет, не эту, не Second Reality а Final Reality.
    Не мог найти ключевые слова, а так — получилось распутать клубок ссылок.
    На Remedy, выпустившую первого Max Payne, работали выходцы из Future Crew. Они-то и выпустили демку Final Reality, которая стилистически (и особенно по музыке) очень похожа на Second Reality.
    Самое примечательное — предпоследняя сцена Final Reality (полёт космического корабля по городу) — это новая версия сцены, присутствующей и в Second Reality.
    В городе из Final Reality можно увидеть баннер с Max Payne, который выйдет только в 2001-м году, тогда как демка выпущена ещё в 1997. В 1997-м году ещё не вышла Матрица, из которой Max Payne получил свою самую примечательную особенность — Bullet Time. Но съёмки матрицы начались как раз в октябре 1997. Теперь даже интересно — имела ли Remedy какие-то связи с киноиндустрией.

    • Kircore
      /#21595646

      В 1997-м году ещё не вышла Матрица, из которой Max Payne получил свою самую примечательную особенность — Bullet Time.

      Замедление было ещё у Джона Ву, откуда Вачовские его и взяли.

  3. axe_chita
    /#21595424

    Не стареющая классика демосцены, великолепно выглядит снаружи, а внутри всё ещё интереснее.