God of War для PSP запустили в браузере на WebAssembly без эмулятора

God of War для PSP запустили в браузере на WebAssembly без эмулятора

На GitHub опубликован проект psp-web-recomp: игры для PSP запускаются в браузере без эмулятора. Машинный код MIPS заранее (AOT) переводится в C++, компилируется в WebAssembly и линкуется с небольшой собственной реализацией операционной системы PSP и её графического чипа, который рисует через WebGL2. Статическую рекомпиляцию выполняет PSPRecomp (лицензия MIT): проект добавляет к нему несколько исправлений и новый целевой профиль для веба. WebAssembly собирает Emscripten.

Первой игрой стала God of War: Chains of Olympus. По описанию в README, она проходит от загрузки через меню, видеоролики-сцены на движке и бои, работают музыка, речь и звуковые эффекты, а вставки с видеороликами (movies) пока пропускаются. В замеренных на данный момент сценах игра держит 60 кадров в секунду в Chrome и Firefox на ноутбуке, при разрешении до четырёх раз выше родного для PSP (рендерер поддерживает от одного до четырёх раз относительно 480×272). Игра работает и на телефонах с экранными сенсорными кнопками. Следом тем же набором скриптов подняли God of War: Ghost of Sparta: ей понадобились расшифровка DRM PSP для одного небольшого файла (176 байт), несколько системных вызовов и исправление освещения, а оптимизация не потребовалась, 55-60 кадров в секунду при тройном разрешении, кадр обходится в 6-8 мс, как и в Chains of Olympus.

В репозитории нет игровых данных: пользователь приносит образ диска игры, которой владеет, а скрипты на его машине превращают его в веб-страницу. Данные диска подгружаются по HTTP Range-запросам, поэтому заранее скачивается только исполняемый файл. Готовой онлайн-версии проект не предлагает: страницу нужно собирать локально. Сборка занимает несколько минут (God of War, около четырёх на 8-ядерном ноутбуке) и около 2 ГБ диска на игру. Всё запускали на Linux; на macOS сборка браузерной версии, по словам авторов README, должна работать, но не проверялась.

Устройство. Вместо эмулятора с интерпретацией, «маленькое ядро PSP»: высокоуровневая эмуляция потоков с семафорами, флагами событий и колбэками, разделов памяти, файловой системы, контроллера, аудиовывода, сохранений и диалогов. Время гостя идёт кадрами, так что игра видит ровно 60 Гц независимо от скорости хоста. Графический чип GE декодирует списки отрисовки на CPU (форматы вершин, скиннинг, освещение, отсечение), затем батчи треугольников уходят в WebGL; фреймбуферы становятся рендер-целями WebGL, а переинтерпретация форматов пикселей, трафарет в альфа-канале, туман и блочные копирования эмулируются на GPU. Звук: реализация синтезатора голосов PSP (32 голоса ADPCM с питчем и ADSR-огибающими), музыка и речь, потоки ATRAC3+, декодируемые кодеком из FFmpeg, вывод идёт в AudioWorklet. GE и его контекст WebGL работают в отдельном рабочем потоке с OffscreenCanvas, как на самой PSP: кадр стоит столько, сколько занимает более загруженный поток, а не сумму обоих. Для потоков нужен SharedArrayBuffer, поэтому страницу надо отдавать с заголовками COOP same-origin и COEP require-corp; браузеры без WebGL2 на OffscreenCanvas автоматически переходят на один поток.

Как дошли от 6 до 60 кадров. Первая играбельная сборка давала 6 кадров в секунду. По README, основная часть пути до 60, выяснение того, куда реально уходит время, а не ускорение рендерера. Что нашли: God of War подменяет фреймбуфер, не дожидаясь вертикального гашения, и рисовала около восьми кадров на каждый показанный; удержание потока, дважды переключающего буфер за одно гашение, до следующего (приём есть и в PPSSPP) само по себе сократило работу на показанный кадр в восемь раз. Таймеры профилирования читали std::chrono на каждый примитив, в WebAssembly это через clock_gettime и преобразование BigInt в JavaScript, и съедало около трети кадра; теперь performance.now() читается только при включённом профилировании. Firefox копирует каждую загрузку буфера WebGL в GPU-процесс и перепроверяет весь индексный буфер после изменения, поэтому общее 4-МБ кольцо индексов уронило Firefox до 3 кадров в секунду; отдельный небольшой индексный буфер на каждую отрисовку это исправил. Обратный приём, писать вершины по частям в один большой буфер, не помог ни одному браузеру и вызвал подвисания на телефонах. Трафарет PSP живёт в альфа-канале фреймбуфера (игра применяет его для проекционных теней и ограничения размытия); зеркалирование трафарета и альфы полноэкранными проходами стоило 60 млн лишних пикселей на кадр при 4×, а отслеживание реально изменившихся областей, битов трафарета и констант снизило это примерно до 7 млн. Наконец, перенос GE в отдельный поток превратил стоимость напряжённого боя в Firefox из суммы «игра плюс графика» (около 17 мс) в максимум из двух (около 10 мс).

Отладка Ghost of Sparta. Игра сначала бесконечно ждала на загрузке: она открывает файл в 176 байт с флагом DRM, передаёт ключ в sceIoIoctl и проверяет прочитанное. Файл в формате PGD, который расшифровывается криптодвижком KIRK; для дисковых игр это AES-128 с тремя ключами из хранилища KIRK (проверка заголовка на основе CMAC и режим счётчика для данных), реализовано в profile/host/pgd.cpp. Затем нашлось белое небо и белая дымка в меню: облачный слой рисуется с включённым освещением, а его непрозрачность берётся из альфы глобального рассеянного света, этот множитель код освещения не учитывал.

Ограничения. Игр пока две, обе от Ready at Dawn на одном движке, так что о совместимости игр других студий это говорит мало. Скорее всего, другая игра остановится на нереализованном системном вызове (он пишется в лог как [hle] unimplemented ...) или использует функцию GE, которую рендерер не поддерживает. Исполняемые файлы на коммерческих дисках зашифрованы, для расшифровки нужен внешний инструмент (например, DecEboot или pspdecrypt) либо дамп из PPSSPP. Сгенерированные C++ и WebAssembly, переводы исполняемого файла игры и принадлежат владельцам игры: хранить их надо у себя и не публиковать. Проект не связан с Sony и другими издателями; код репозитория, под лицензией MIT, встроенный at3_standalone сохраняет LGPL 2.1.

Ключевые факты

  • Игры PSP запускаются в браузере без эмулятора: код MIPS заранее переводится в C++, затем в WebAssembly и работает поверх малой реализации ОС PSP и отрисовки через WebGL2.
  • God of War: Chains of Olympus проходит от загрузки до боёв, держит 60 кадров/с в замеренных сценах (Chrome и Firefox на ноутбуке) при разрешении до четырёх раз выше родного; работает и на телефонах с сенсорным управлением. Видеоролики пока пропускаются.
  • God of War: Ghost of Sparta заработала следом: понадобились расшифровка DRM для одного файла, несколько системных вызовов и правка освещения; 55, 60 кадров/с при тройном разрешении.
  • От 6 до 60 кадров/с игру довели в основном профилированием: удержание переключения буфера до вертикального гашения (восьмикратное сокращение работы на кадр), отказ от медленных таймеров, отдельный поток для графики (в напряжённом бою в Firefox около 17 мс → около 10 мс).
  • Игровых данных в репозитории нет: нужен собственный образ диска, сборка идёт локально (около 2 ГБ на игру), проверены только две игры одной студии, готовой онлайн-версии нет.

Почему это важно

Это показывает, что относительно тяжёлую игру для портативной консоли можно перенести в браузер не через эмулятор с интерпретацией, а через предварительную рекомпиляцию в WebAssembly, и получить 60 кадров в секунду на обычном ноутбуке. Отдельно ценен разбор оптимизации: по README, основная часть ускорения с 6 до 60 кадров пришла от выяснения, куда уходит время, а не от ускорения рендерера. Конкретные находки (удержание потока до вертикального гашения, медленное чтение часов в WebAssembly, особенности буферов WebGL в Firefox и на мобильных GPU) применимы и вне этого проекта.

Кому это важно

Разработчикам, которые занимаются WebAssembly, WebGL и портированием нативного кода в браузер; тем, кто интересуется статической рекомпиляцией и эмуляцией PSP; энтузиастам сохранения игр. Обычным игрокам проект пока мало что даёт: готовой онлайн-версии нет, страницу нужно собирать самому из собственного образа диска.

Как это применить

Нужны git, CMake, Ninja, компилятор C++20 и Python 3. Порядок: клонировать репозиторий, выполнить scripts/setup.sh (ставит PSPRecomp, патчи и Emscripten SDK), затем scripts/port.sh с указанием инструмента расшифровки исполняемого файла (например, DecEboot или pspdecrypt) и образа диска, затем scripts/serve.sh, после чего страница открывается на localhost:8613. Сборка занимает несколько минут и около 2 ГБ диска на игру. Страницу нужно отдавать с заголовками COOP same-origin и COEP require-corp (scripts/serve.py делает это сам). Параметр ?profile добавляет разбивку времени кадра?threads=0 принудительно включает один поток. Репозиторий распространяется под лицензией MIT.

Можно ли доверять

Все цифры (60 кадров/с, 55-60 кадров/с, 6-8 мс на кадр), заявления самого README проекта, источник не называет конкретных моделей ноутбука и версий браузеров, а замеры сделаны, по формулировке README, «в замеренных на данный момент сценах». В тексте приложена запись на 1:46. Авторы сами оговаривают, что проверены только две игры одной студии на одном движке. Подтверждений от независимых сторон в источнике нет.

Риски и подводные камни

Совместимость с другими играми не проверялась: вероятно, они остановятся на нереализованном системном вызове или неподдерживаемой функции графического чипа. Видеоролики пока не воспроизводятся. Всё запускали на Linux, на macOS сборка не проверена. Сгенерированные C++ и WebAssembly принадлежат владельцам игры, их нельзя публиковать; нужны диски игр, которыми вы владеете. Если выставить страницу через туннель, любой с адресом сможет загрузить игру, пока туннель работает. Проект заявляет, что не связан с Sony и другими издателями.

«Большая часть пути до 60 кадров в секунду пришла от выяснения, куда на самом деле уходит время, а не от ускорения рендерера.»

— README проекта psp-web-recomp