А вы знали, что теперь можно нативно запускать Microsoft 3D Movie Maker в Linux? В течение последних полутора лет я работал над 3DMMEx — моим портом исходников 3D Movie Maker. Одной из целей моего форка была портируемость. Недавно проект достиг важного этапа: теперь он компилируется и запускается в Linux, благодаря чему 3DMMEx стал первым из известных форков 3D Movie Maker, работающих вне Windows! В этом посте я расскажу о некоторых из трудностей, с которыми я столкнулся при портировании на новую платформу написанного тридцать лет назад мультимедийного приложения.

Предыстория

В 2020 году я написал серию постов о реверс-инжиниринге Microsoft 3D Movie Maker. С тех пор произошло важное событие: в мае 2022 года Microsoft опубликовала полный исходный код приложения. Это оказалось довольно неожиданным: я не думал, что когда-нибудь смогу увидеть оригинальные исходники и вообще не верил, что они сохранились в архивах Microsoft, однако теперь они на GitHub… и выложены под разрешающей лицензией MIT!

Для начала я хотел бы поблагодарить Элис Эверлонг за её неустанное исследование исходников 3DMM, а также Скотту ХэнселменуДжеффу Уилкоксу и другим членам команды Microsoft, благодаря которым произошёл этот релиз. Если вам любопытна история выпуска исходного кода, то послушайте посвящённый этому выпуск подкаста Hanselminutes.

В репозитории содержится исходный код приложения 3DMM (по кодовым названием Socrates), фреймворк приложений Kauai, использовавшийся для 3DMM и Creative Writer 2, инструменты разработки/мультимедиа (в том числе компилятор скриптового языка, реверс-инжиниринг которого я выполнил), документация и отрендеренные ассеты. В релизе есть всё необходимое для компиляции собственной сборки 3DMOVIE.EXE.

После выпуска релиза я изучил репозиторий и написал примечания об исходном коде, в том числе и о том, как собрать его при помощи оригинальных инструментов разработки. Я сразу же заметил высокое качество кодовой базы: согласованный стиль кодинга, всё задокументировано и есть куча проверок assert. В 1995 году разработчики не имели возможности выпускать апдейты для устранения багов, поэтому им приходилось тщательно отлаживать код перед релизом. Кодовая база 3DMM намного чище, чем релизы исходного кода других игр середины 90-х, которые я видел.

Первым делом после релиза кода я задумался о возможности портировать его на другую платформу. Мне не хотелось писать собственный форк (для этого понадобилась бы уйма труда!), поэтому я присоединился к проекту 3DMMForever, запущенного с целью модернизации 3DMM вскоре после релиза. Мы заменили старые makefile на CMake и решили кучу проблем, чтобы 3DMM компилировался современным компилятором Visual Studio 2022. Всё это стало хорошей основой на пути к созданию портируемого 3DMM.

К сожалению, вскоре после этого проект затормозил... поэтому в конце 2024 года я форкнул 3DMMForever, чтобы создать 3DMMEx. Изначально я планировал устранить отдельные мелкие проблемы 3DMMForever и изучить, что необходимо для портирования 3DMM на другие платформы. Основные проблемы, усложнившие процесс портирования, заключались в следующем:

  • Код был написан на устаревшем, нестандартизированном диалекте C++, который компилировался только Microsoft Visual C++.

  • Фреймворк приложений Kauai был «кроссплатформенным», но поддерживал только две платформы: Windows и Macintosh 68K. Впрочем, поддержка Macintosh была очень сырой.

  • Приложение 3DMM создавалось поверх кроссплатформенных абстракций Kauai, но иногда прорывалось сквозь них и обращалось непосредственно к Win32 API.

  • В требующих высокой производительности функциях фреймворка использовался встроенный язык ассемблера x86.

  • В проекте делаются допущения о размерах указателей, что на 64-битных системах вызывало ошибки компиляции.

  • Фреймворк имел большой объём функциональности, не используемой в 3DMM, но требуемой для мультимедийных инструментов.

  • Некоторые внешние зависимости скомпонованы в сборку в виде скомпилированных статических библиотек.

  • В коде почти нет тестов, что усложняет обнаружение регрессий. В процессе разработки создатели 3DMM использовали автоматизированное тестирование и даже оставили в коде часть хуков автоматизированных тестов, но в опенсорсный релиз тесты не вошли.

Если взять исходный код в его оригинальном виде и попытаться скомпилировать его для другой платформы, то придётся столкнуться со всеми этими проблемами одновременно. Поэтому я решил устранить часть проблем изолированно на оригинальной сборке для Windows x86. Я подумал, что если решу некоторые из этих проблем, то, возможно, мои труды окажутся полезными для тех, кто захочет портировать код на другую платформу.

Вскоре после того, как я создал свой форк, мне пришло письмо от потрясающего разработчика Марка Кейва-Эйленда. Марк хотел портировать 3DMM в Linux, чтобы можно было запускать приложение на Raspberry Pi для его детей. Я решил, что это отличная идея! Больше года мы вместе работали над портированием 3DMM на Linux: Марк занимался портированием на Linux, а я — портированием на SDL в Windows.

Статические библиотеки

В проекте 3DMM было две внешние зависимости, упакованные в виде статических библиотек: BRender и AudioMan. BRender — это движок 3D-рендеринга, разработанный Argonaut Technologies. Исходный код BRender был опубликован примерно в то же время, что и код 3DMM, после того, как Элис Эверлонг получила одобрение у Джеса Сана, бывшего CEO Argonaut Technologies. BRender можно собрать из исходников, но он содержит существенный объём развёрнутого вручную кода на языке ассемблера x86. Контрибьютор 3DMMForever @prettytofugirl проделала отличную работу по замене всего ассемблерного кода x86 на портируемый код C, который я использую теперь в 3DMMEx.

AudioMan — это библиотека аудиомикшера, которая использовалась во множестве мультимедийных продуктов Microsoft середины 90-х. AudioMan применяется в 3DMM не только для воспроизведения звука, но и для преобразования импортированного и записанного аудио. Статическая библиотека не работала, когда её компилировали для любой другой платформы, кроме Windows x86 и Visual Studio. Поэтому я решил начать собственный мини-проект по декомпиляции! За пару выходных я при помощи Ghidra выполнил реверс-инжиниринг статической библиотеки AUDIOD.LIB и декомпилировал её на C++. Полную декомпиляцию обычно выполнять довольно сложно, но в данном случае у меня была отладочная сборка библиотеки с полной символьной информацией. Кроме того, для правильной работы 3DMM мне не требовалась библиотека целиком, поэтому я декомпилировал только критические компоненты. Это был довольно интересный сторонний проект; думаю, стоит написать о нём отдельный пост.

Декомпиляция AudioMan стала важной частью сборки версий 3DMMEx для Windows x64 и ARM64. Однако оказалось, что библиотека тесно связана со звуковыми API и интерфейсами COM Windows, поэтому было бы сложно заставить её работать на других платформах, поэтому для них я добавил новый модуль воспроизведения волнового аудио на основе miniaudio. Кроме того, интеграция miniaudio решила проблему кроссплатформенного ввода аудио, который требовался для функций записи звука в приложении.

Избавляемся от ассемблерного кода

Некоторые функции в Kauai содержат оптимизированный вручную ассемблерный код. Он используется в функциях, требующих высокой производительности, например при копировании памяти, манипуляциях с растровыми изображениями или сжатии данных. Для переключения между реализациями на C++ и языке ассемблера используются #define времени компиляции. Некоторые функции имели версии, оптимизированные и под Intel x86 для Windows, и под Motorola 68020 для Macintosh.

В оригинальной кодовой базе также содержался генератор кода, создающий версии двух специализированных алгоритмов сжатия Kauai на языке ассемблера x86. Помню, что когда я только приступил к реверс-инжинирингу 3D Movie Maker, то нашёл в IDA функцию распаковки KCD2 и был немного напуган её сложностью. Я подумал, что её точно писал не человек. И оказался прав!

Граф потока управления KCD2 в IDA
Граф потока управления KCD2 в IDA

Перейти на версии на C++ оказалось достаточно просто, всего лишь нужно было удалить #define, но при этом обнаружилось несколько багов в соответствующих реализациях на C++. Кроме того, устранение ассемблерных версий привело к небольшим улучшениям производительности, поскольку приложение теперь могло пользоваться реализациями memcpy/memmove на C, оптимизированными под современные процессоры.

Скучные инженерные подробности

Для портирования 3DMM на новые платформы требуется множество изменений, которые, скорее всего, поломают старый код. Перед внесением серьёзных модификаций я решил уделить время процессу отладки и тестирования, надеясь, что это поможет мне в дальнейшем отлавливать регрессии. Я внёс следующие изменения:

  • У фреймворка Kauai было небольшое количество юнит-тестов. Я портировал их в Google Test и интегрировал в сборку.

  • Kauai использует собственные типы данных, которые сложно читать в отладчике Visual Studio. Я написал визуализаторы NatVis, чтобы подсказать Visual Studio, как рендерить отдельные типы. Например, строковый класс (STN) теперь показывает значение строки, а не указатель на буфер.

  • Я старался не вносить изменения, которые бы поломали совместимость форматов файлов, чтобы пользователь по-прежнему мог загружать и сохранять файлы фильмов, созданные тридцать лет назад, поэтому добавил статические assert вокруг структур форматов файлов, чтобы они не менялись в размерах при компиляции на другие платформы. Эти assert помогали находить проблемы при портировании 3DMM на 64-битные системы.

  • UI приложения было сложно отлаживать из-за того, что основная часть логики находится внутри скриптов. Я добавил дополнительный логгинг для отображения текущего исполняющегося скрипта, чтобы было проще следовать за логикой UI в процессе её исполнения.

  • Я внёс множество небольших рефакторингов, чтобы добавить границы между компонентами; это позволило добавлять новые реализации для SDL/кроссплатформенных сборок без удаления исходных работающих реализаций для Win32.

Строковые объекты с визуализаторами NatVis и без них в отладчике Visual Studio
Строковые объекты с визуализаторами NatVis и без них в отладчике Visual Studio

Также я решил сохранить стиль кодинга Apps Hungarian, который применяли разработчики оригинала в 1990-х. Возможно, в 2026 году это кажется немного странным, но мне хотелось, чтобы новый код был согласован со старым. Нужно какое-то время, чтобы к нему привыкнуть, но поработав какое-то время над кодовой базой, вы не задумываясь начнёте использовать имена переменных наподобие mpgrfchpsz!

Заменяем Win32 на SDL

Библиотека GUI Kauai рассчитана на использование Win32 API в Windows и Macintosh Toolbox в System 7. Я заменил реализацию GUI Win32 на SDL — популярную опенсорсную библиотеку для кроссплатформенных мультимедийных приложений.

Чтобы упростить тестирование SDL, я начал с написания маленького приложения «hello world» на Kauai, тестирующего отправку событий и базовый рендеринг. Благодаря этому для тестирования изменений во фреймворке мне не понадобилось обеспечивать корректную работу всего приложения. При помощи тестового приложения я научился создавать окна и обрабатывать простейший ввод, после чего начал работать над рендерингом.

SDL-бэкенд преимущественно довольно прост: в Kauai уже есть довольно хорошие абстракции фич GUI. Например, вся графика обрабатывается в классах GNV (Graphics Environment) и GPT (Graphics Port). У этих классов есть методы наподобие DrawRcs для рисования прямоугольника или DrawRgch для рендеринга текста. Мне достаточно было заменить код рендеринга Win32 GDI на эквиваленты из SDL.

Иногда это было сложновато, потому что некоторые операции, требовавшие в Win32 одной строки кода, занимали сотни строк нового кода на SDL. Хороший пример этого — работа со шрифтами. Во многих мультимедийных приложениях 90-х использовался только постоянный набор шрифтов, упакованный вместе с ассетами приложения. Однако 3DMM хочет находить и использовать все доступные в системе шрифты TrueType. В Win32 API для создания списка шрифтов достаточно одного вызова EnumFonts, обеспечивающего обратный вызов для каждого нового шрифта. Затем можно вызвать CreateFontIndirect, чтобы получить дескриптор шрифта для рендеринга текста. В SDL нет возможности создания списка шрифтов, поэтому её нужно реализовывать для каждой платформы.

Ещё один пример — это обработка горячих клавиш. В Win32 есть таблицы акселераторов, обеспечивающие отображение между кодами виртуальных клавиш и сообщениями WM_COMMAND. Таблицы акселераторов обычно хранятся в разделе ресурсов исполняемого файла. При запуске приложения код вызывает LoadAccelerators для загрузки таблицы, а затем вызывает TranslateAccelerator в цикле событий, чтобы транслировать команды нажатия клавиш в WM_COMMAND. Всю работу с таблицей выполняет за нас Windows. Чтобы горячие клавиши работали в сборках на SDL, мне пришлось самостоятельно реализовать систему таблиц акселераторов. Плюс такого подхода в том, что он сильно упрощает поддержку настраиваемых комбинаций клавиш. Я планирую добавить их поддержку в будущем релизе.

Довольно поздно я понял, что SDL ожидает строки в кодировке UTF-8. В 3DMM используется кодовая страница системы по умолчанию (CP1252 для системы с английским языком). Из-за этого текст выглядел почти правильно, но потом я нашёл часть UI, в которой длинное тире рендерилось, как прямоугольник. Я добавил методы для преобразования из/в UTF-8. Также 3DMM поддерживает Unicode с UCS-2, но он крайне поломан в оригинальном релизе исходников; его починили только в японском релизе 1996 года. В будущем я бы хотел заменить всю внутреннюю обработку строк на UTF-8, но это потребует фундаментальных изменений в процессе сериализации фильмов.

Рисуем остальную часть совы

Избавившись от встроенного ассемблерного кода и реализовав SDL-бэкенд, мы достигли этапа, на котором 3DMMEx мог запуститься нативно на другой платформе. Здесь огромный объём работы проделал Марк, решив множество проблем, от обработки имён файлов с учётом регистра и обратных косых черт до написания новых модулей для поддержки платформы POSIX, интеграции FluidSynth для воспроизведения MIDI и GStreamer для воспроизведения катсцен. Спустя несколько месяцев Марк смог запустить 3DMMEx в Linux! Из серии постов Марка о портировании 3DMM в Linux можно узнать и о других технических трудностях, с которыми мы столкнулись.

Выражаю Марку огромную благодарность не только за отличную работу над портированием для Linux, но и за моральную поддержку, позволившую мне продолжить трудиться над 3DMMEx. Портирование ПО на другую платформу может быть довольно длительным процессом, поэтому сотрудничество с опытным мотивированным разработчиков вроде Марка сделало его намного проще и увлекательней!

Если хотите попробовать 3DMMEx, то перейдите в наш проект на GitHub. Двоичные файлы Windows для x86, x64 и ARM64 доступны на странице Releases. Пока для запуска в Linux придётся собирать их из исходников. но я планирую создать двоичные релизы для самых популярных дистрибутивов Linux.

Хоть в порте SDL/Linux работают все фичи 3DMMEx, предстоит ещё внести некоторые улучшения. Пока самая большая проблема заключается в обработке мыши при перетаскивании актёров по сцене: некоторыми устройствами ввода совершенно невозможно пользоваться. Было бы здорово, если бы кто-то портировал проект и на другие платформы, например, macOS и может даже Emscripten для запуска его в браузере. Если вы хотите помочь с этим, то зайдите на страницы issues или discussions в GitHub.

А в конце покажу видео компиляции и запуска 3DMMEx в Linux!

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