Как было дело давным‑давно я увидел демку Micropolis (вы тоже можете глянуть ее на Youtube), и она меня покорила. Там был целый город, пусть квадратный, но все же. Там была река, мосты, поезд, воздушный транспорт. Робот. Нет, РОБОТ. И он даже шагал. Под музыку. И все это в 4 килобайтах. Да, не мегабайтах, и даже не 64, а именно 4. Тогда я не смог понять, как это работает, но вот недавно решил разобраться.

Естественно, первым слоем нужно было снять упаковку — так как очевидно, что данное демо запаковано. Но оказалось, что делать этого не нужно — так как запакована она очень хитрым способом. Вся демка это просто файл.bat длиной 4092 байта. Естественно всякую фигню в начале Windows пропустит ибо таких команд у него и в помине не было, а потом он перейдет в конец и увидит обычный батник — который копирует демку во временный каталог и распаковывает ее стандартной Windows‑программой Expand, в результате чего мы получаем файл длиной 9571 байт, уже ничем не упакованный.

Вот такой странный .bat распаковывал демо
Вот такой странный.bat распаковывал демо

Но если мы попытаемся его запустить, ничего не заработает, потому что демка была написана во времена Windows XP SP1, и ориентируется именно на него. Поэтому в процессе декомпиляции я немного изменил ее, адаптировав под современные реалии:

  1. Импорты. Как обычно все демо импортируют нужные им функции. Обычно не по именам, так как это, очевидно, занимает слишком много места. Новые демо чаще берут 32-битный хэш от нужных имен функций, и сами настраивают импорт используя функцию GetProcAddress. Однако здесь авторы пошли еще дальше, и импортируют функции просто по ординалам (их номерам внутри.DLL‑библиотек). Очевидно при любой новой версии библиотек данная схема слетает, и нигде кроме оригинальной ОС это работать не будет. Поэтому заменил на обычные импорты.

  2. Чтобы создать окно в Windows нужно сначала создать класс окна. Но так как это, опять же, место — то в данной демо используется стандартный класс окна под названием Edit (он используется, например, в блокноте). Поэтому, если во время демо вы будете нажимать что‑то на клавиатуре — в верхнем левом углу будут появляться символы — как будто в блокноте — это цена за использование данного класса. Но в чем проблема? В том, что данный класс — как и любой другой — получает сообщения от Windows — вроде «нажали кнопку», «навели мышку», «сменился размер окна» и прочие. Старая демка просто игнорировала их — и на Windows XP это работало. А вот на новых ОС уже нет. Поэтому пришлось добавить небольшой код который получал сообщения, и просто их выбрасывал.

  3. Иногда при воспроизведении на полный экран поверх демо вылазила панель задач и портила все впечатление. Поэтому пришлось сделать окно TOPMOST — опять же, потратив немного кода. А также заблокировать фичу новых Windows когда они могут увеличивать все что есть на дисплее на 125, 150 и так далее — процентов. Это, конечно, хорошая функция, но в данном случае она портит всю композицию демо.

  4. Изначально демо выполнялось в окне 640×360 — чего для современных реалий, конечно же, мало. Поэтому все было расширено до стандартных 1920×1080 — и даже код менять не пришлось, так как что там, что там — 16:9, а вся графика все равно вычисляемая. Более того — окно пришлось сделать 1921×1080 — потому что если делаешь 1920×1080 — у Windows срабатывает какое‑то правило «программ для полноэкранного режима» — и она принудительно переводит их обратно в оконный режим. Одна лишняя точка по горизонтали приводит к тому что программа уже не считается полноэкранной для Windows, и все проходит гладко. Возможно вы знаете, что это за поведение, и как это решить более элегантно.

Итак, что же я нашел внутри самой демки. Внутри оказался OpenGL, причем самой базовой версии — то есть — без всяких там шейдеров и прочего нового, модного и молодежного.

Представьте, у вас есть 9,5 килобайт. Из них один килобайт ушел на заголовки EXE и ничего полезного там нет. Остается 8,5 килобайт — то есть — примерно по 4Кб на код и на данные. Логично подумать, что большую часть кода будет занимать графический движок? Я тоже так думал. А оказалось… нет. У нас есть:

  • Почти 400 байт кода и 200 байт данных инициализации. Создать окно, создать контекст OpenGL, сменить видеорежим, перевести на полный экран — все это требует места.

    Сколько же бойлерплейта нужно чтобы просто запустить демо
    Сколько же бойлерплейта нужно чтобы просто запустить демо
  • Почти 1,5 килобайта кода и не меньше данных для… музыки. Музыка тут тоже вычисляемая, и довольно сложная. Тут не просто какой‑нибудь WAV — а целый оркестр из четырех каналов, каждый из которых играет свою партию. Партии тоже описаны отдельно и задаются нотами. Плюс на все это накладывается эхо (которое берется из той же мелодии, но со смещением и уменьшением громкости), и даже есть пост‑обработка фильтром в конце.

  • Целая процедура и куча данных о вершинах для построения кубика размером 1×1х1 (который потом растягивается по осям чтобы получить все остальные фигуры кроме цилиндров). Я был в шоке, когда узнал, что цилиндр — встроенный примитив OpenGL, а вот кубик нет — стройте сами.

    Хотел от этого избавиться, но нет - оказалось это не встроенная фича. А перевод на байты только ухудшает компрессию
    Хотел от этого избавиться, но нет — оказалось это не встроенная фича. А перевод на байты только ухудшает компрессию

Ну а теперь — как же все это уместилось в таком объеме:

  1. Очень плотный формат хранения данных. На каждый куб уходит всего 9 байт. 3 байта на его положение (X, Y, Z), три байта на поворот (в градусах, опять же, X, Y, Z), и еще три байта — размер (опять же, X, Y, Z). Все. Ничего лишнего. Цилиндр задается точно также, только кроме размеров у него «высота цилиндра, радиус цилиндра, и… ноль». Третье поле для цилиндров не используется

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

    Вот вам и весь робот - ссылки на геометрию каждой части
    Вот вам и весь робот — ссылки на геометрию каждой части
  3. Движение каждой части робота описывается обычными синусоидами Offet + Amplitude sin(scene_time Frequency_multiplier + Phase PI/128). Данная формула применяется как для перемещений по каждой из осей, так и для поворотов (опять же, вокруг каждой из осей). Соответственно, так как все 4 необходимые переменные занимают лишь по байту на каждую, получаем: 4 переменные 6 синусоид для каждой части робота * 10 частей робота = 240 байт. А так как таких таблиц две (одна для вставания робота и одна для ходьбы) — то всю анимацию робота упихали в 480 байт.

    И вот так параметры синусоиды для каждой части
    И вот так параметры синусоиды для каждой части
  4. В процессе демо постоянно двигается камера. И это тоже описано в виде «пресетов камеры» длиной по 17 байт — всего лишь описывающих положение камеры, ее поворот, и скорость движения и поворота со временем (а также цвет тумана и привязку к какому‑либо объекту).

    Вот и все настройки камеры
    Вот и все настройки камеры
  5. Сами сцены вообще описаны лишь четырьмя байтами. Первый — длина сцены в чанках по 435 миллисекунд. Второй — что нужно включить в сцену — кроме постоянно присутствующих объектов. Есть четыре варианта — ничего, собранного робота, встающего робота, идущего робота (последний вариант также подразумевает едущий поезд). Третий — как уже указывалось в пункте 4 — номер пресета камеры. И самое главное — четвертый указывает текущее время. Мне не сразу стало понятно, зачем нужно текущее время, ведь в первом байте у нас уже указана длина, так что мы можем посчитать время демо. Но дело в том, что время «для зрителя» и время «демо» это разные вещи. То что вам кажется жизнью города — на самом деле — его слепки в разные моменты времени. Отчетливее всего это видно когда где‑то ближе к концу поезд дергается три раза в так музыке, хотя он должен был двигаться гораздо медленнее. Это просто «машина времени». Точно также, робот в конце демо как будто чуть не врезается в здание, а в следующем кадре идет как ни в чем ни бывало на середине моста — это тоже просто перемотка времени назад.

    Вот и весь "плейлист" демки
    Вот и весь «плейлист» демки

Ну а теперь — самое вкусное. Не мог же я просто декомпилировать демо, сказать — какая она крутая, и на этом закончить. Нет, я попытался ее улучшить. Тут пошло в ход несколько вещей — замена FLOAT‑значений на байты — например — для цветовой палитры, избавление от кучи лишних байтов 0CCh — видимо вставленных компилятором C, а также переписывание прологов функций, где куча ненужных команд PUSH/POP, и много чего еще. Плюс, после всего этого я сжал ее новым инструментом для сжатия демок — Crinkler (кстати, Crinkler создан тем же Mentor, что и сама демка), и в итоге получил 3766 байт. Так что если кто‑то захочет добавить что‑то новое — у вас теперь есть более 300 байт форы.

И, конечно же, результаты дизассемблирования лежат в полностью открытом виде со всеми комментариями на гитхаб с разрешения авторов. Так что — смотрите, разбирайтесь, пишите комментарии. Я вижу еще множество способов ее уменьшить, например — в некоторых местах есть еще неиспользуемые байты, структуры DEVMODEA и PIXELFORMATDESCRIPTOR вполне можно совместить, засунув одну в другую, код для поворота вокруг каждой из осей, используемый трижды, можно вынести в общую функцию. Но — когда я все это делал — код становился только больше. Видимо Crinkler попал в какой‑то локальный минимум — и ему все мои перестановки не понравились.

В настоящий момент я решил замахнуться на более масштабную вещь — KKrieger, FPS‑игру размером 96Кб от Farbrausch. Я знаю, что у нее уже есть открытые исходники, но они на Си, а это значит, получить минимальную версию не получится. Поэтому хочу дизассемблировать ее и попробовать уместить в 64Кб вместо 96Кб. Но это дело будущего, а Micropolis можно изучать уже сейчас.

Комментарии (1)


  1. becefi
    08.10.2026 13:44

    Мощно.