Прокачиваем NES Classic Mini — продолжение

Habrahabr

В феврале я писал статью о том, как русские энтузиасты «взламывали» консоль NES Classic Mini, как мы занимались её реверс-инжинирингом, и как я писал приложение для того, чтобы каждый мог легко закачать в неё свои игры буквально в пару кликов.

image

Сегодня я напишу продолжение этой истории, ведь за полгода мы добились гораздо более существенного прогресса в этом деле.

Как обычно, под катом моё повествование и в современной видеоформе, и в виде традиционной статьи.

Видео

Статья

Больше эмуляторов

Первый переломный момент настал, когда человек под псевдонимом madmonkey (если кто не помнит, именно он — автор взлома) смог скомпилировать под NES Mini эмулятор RetroArch. Точнее это не эмулятор, а мультиплатформенная оболочка на основе Libretro для запуска других эмуляторов, которые устаналиваются в виде модулей. Благодаря этому на NES Mini теперь можно запустить не только любые игры для NES, независимо от мапперов, но и игры с других консолей: SNES, Sega Mega Drive, Gameboy, Gameboy Advance и даже простенькие игры с Nintendo 64. Работает даже эмуляция картриджа, который я сам разрабатывал, если скомпилировать модуль эмулятора fceux с моими дополнениями. Магия Open Source.

Само собой, появление таких возможностей сопровождалось кучей новых проблем и задач. Например, мне очень хотелось, чтобы эмуляция через RetroArch запускалась не из его собственного меню, а тоже из оболочки NES Mini.

Я уже рассказывал, что для каждой игры в конфиге просто указывается исполняемый файл, который нужно запустить, и параметры командной строки. Но помимо этого родной эмулятор NES Mini взаимодействует с оболочкой, а именно позволяет выходить в меню, сохраняться и менять настройки. Мне хотелось, чтобы все эти возможности сохранялись и при использовании RetroArch. Чтобы пользователь не мог даже понять, что используется сторонний эмулятор. Это потребовало некоторого изучения того, как устроена оболочка в NES Mini. Однако, всё оказалось банально и просто. На самом деле, когда во время игры вы нажимаете кнопку для отображения меню, эмуляция не встаёт на паузу. Оболочка убивает процесс эмулятора, а эмулятор при выходе сохраняет игру во временный файл и делает скриншот. Оболочка же подхватывает эти временные файлы и показывает это как приостановленную игру, которую можно сохранить. В таком случае временные файлы просто копируются в постоянную память. Что же касается настроек, все они просто передаются в параметрах командной строки.

Выходит, что поставленная задача весьма проста: надо написать скрипт для запуска RetroArch, который будет парсить параметры от оболочки NES Mini, запускать эмуляцию, убивать эмулятор по сигналу от оболочки, сохраняя при этом игру в нужное место и делая при этом скриншот. Однако, без костылей не обошлось. Если RetroArch и умеет делать автоматические сохранения, то вот с созданием скриншота при выходе всё не так просто. Да, у RetroArch есть опция для создания скриншотов к сохранениям. Но она не распространяется на автоматические сохранения. Я решил получать скриншот альтернативным методом. В Linux фреймбуфер, в котором содержится изображение с экрана, доступен через псевдофайл "/dev/fb0". Нужно только как-то читать из него данные и сохранять в виде PNG-файла. К счастью, я быстро нашёл для этого готовую утилиту, которая так и называется “fbgrab”. К моему великому удивлению я смог скомпилировать её под NES Mini самостоятельно, но получаемые с её помощью скриншоты были какими-то странными. Почему-то во фреймбуфере NES Mini почти у всех полезных пикселей стоит стопроцентный уровень прозрачности. Пришлось дорабатывать эту утилиту, чтобы она прозрачность игнорировала:

 /* ALPHA */
        outbuffer[(i<<2)+Alpha] = /*srcAlpha >= 0 ? inbuffer[i*4+srcAlpha] :*/ 0;

Да и откуда может быть прозрачность на скриншоте? Надеюсь, кто-нибудь пояснит мне это в комментариях.

Вскоре всё заработало, правда соотношения сторон у скриншотов были немного неправильные, ведь оболочка рассчитана получать скриншот в соотношении четыре к трём, а не шестнадцать к девяти, но это опять же решилось простой доработкой утилиты. Ну или не очень простой. Я сделал автоматическое определение соотношения сторон.

Увы, я не смог подобрать полноценную замену ретро-фильтру, который есть в оригинальном эмуляторе, и который очень реалистично создаёт имитацию CRT-телевизора. Я не люблю подобные фильтры, но в данном случае это смотрится действительно потрясающе и очень реалистично, я больше нигде подобного не видел. Поэтому при возможности я всё-таки отдаю приоритет оригинальному эмулятору NES Mini, но он запускает не все игры.

Напомню, что внутри каждого ".nes" файла с игрой содержится заголовок, в нём описывается железо, которые было внутри оригинального картриджа. Эмулятор должен эмулировать не только саму консоль, но и это железо. Оригинальный эмулятор NES Mini эмулирует только самые популярные варианты картриджей, RetroArch же благодаря модулям может запустить почти всё. Я дописал в скрипт запуска RetroArch код, который через утилиту «hexdump» читает заголовок NES-файла, парсит его и автоматически решает, какой эмулятор запускать — оригинальный или RetroArch:

supported_mappers="0 1 2 3 4 5 7 9 10 86 87 184"
emulator=retroarch

args=$@
filename=$1
extension=${filename##*.}

if [ "$extension" == "nes" ]; then
  header=$(hexdump -v -n 8 -e '1/1 "%02X"' "$filename")
  mapper=$((0x${header:14:1}${header:12:1}))
  fourscreen=$(expr $((0x${header:13:1})) / 8)

  echo mapper: $mapper
  echo four-screen: $fourscreen

  for m in $supported_mappers; do
    [ "$m" == "$mapper" ] && emulator=kachikachi
  done
  [ "$fourscreen" == "1" ] && emulator=retroarch
fi

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

Система модов

Однако, впереди были задачи посерьезнее. Как позволить пользователям максимально легко устанавливать эмулятор и модули? madmonkey предложил разработать универсальную систему модов, которая была бы совместима и с его hakchi, и с моей hakchi2, и позволила бы при этом максимально гибко в виде плагинов устанавливать на NES Mini самые разные хаки: от банальных скинов до эмуляторов и альтернативных оболочек. Это была череда бессонных ночей, когда мы с madmonkey сидели в IRC и обсуждали, как лучше всё сделать. При этом он писал очень красивый код, а я его тестировал, находил ошибки и исправлял их с помощью своего ужасного кода. Затем он заменял мой кривой код на свой, после чего всё повторялось заново. Так продолжалось неделю, в итоге система модов была готова.

Мы условились, что модули должны иметь расширение “.hmod”, являясь при этом на самом деле “.tar.gz” архивами, которые содержат в себе необходимые файлы и скрипты, выполняющиеся при установке и удалении. При этом в самом hakchi был реализован набор функций для копирования и подмены файлов. Позже я написал подробный гайд по созданию модов с примерами, начиная с банального редактирования интерфейса и вплоть до того, как поставить пароль на вашу NES Mini, можете почитать его на GitHub’е: github.com/ClusterM/hakchi2/wiki/Modifications-and-modules-guide (на английском).

Всё это позволило комьюнити участвовать в создании и распространении самых разных модификаций. Так в сети скоро стали появляться и скины, и модули для перевода Фамикома на английский язык, и много чего ещё. Кстати, madmonkey сделал очень лёгким процесс превращения NES Mini в Famicom Mini, равно как и наоборот. Я же перенёс в модули часть функционала hakchi2 вроде расширенных шрифтов и драйвера контроллеров. Ну и конечно же самым первым модом представленным общественности был описанный выше эмулятор RetroArch и плагины к нему для эмуляции разных консолей. Надо отдать должное человеку с ником pcm720, который взял на себя ответственность развивать проект мода дальше, я даже дал ему полные права на GitHub’е. Здорово, когда находятся такие люди, ведь сложно следить сразу за несколькими проектами, мне же хотелось продолжать работу над hakchi2. Мод RetroArch на GitHub: github.com/ClusterM/retroarch-clover

Кстати, hakchi2 конечно же пришлось тоже дорабатывать. И если с установкой и удалением модулей всё понятно, то вот с играми всё было не совсем ясно. Я очень уж не хотел добавлять прямо в hakchi2 поддержку игр с платформ отличных от NES. Всё-таки это нестандартная фишка, добавляемая отдельным модом, который в комплекте не идёт. Однако, огромное сообщество пользователей этого очень хотело, да ещё с иконками, обложками и остальными фишками. Так что я сдался. Сделал для каждой консоли отдельный класс со своими особенностями. а человек под ником NeoRame нарисовал кучу классных картинок под каждую консоль, за что ему огромное спасибо.

Можно было проще

А дальше было самое интересное. Когда люди стали записывать сотни разных игр с разных платформ, постоянно что-то добавляя или удаляя, они столкнулись с проблемой, что каждый раз, даже при добавлении всего одной игры, необходимо закачивать все игры заново. Ведь, как я уже рассказывал в прошлой статье, мы не имеем никакой обратной связи с консолью. Мы лишь берём кучу игр, скрипт для их установки и запускаем это дело на NES Mini, предполагая, что уже установленные игры должны удалиться, а новые установиться. У нас нет никакой возможности узнать, какие игры уже установлены.

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

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

Размышляя над этой задачей, я постепенно продолжал изучать устройство системы NES Mini и заметил, что в автозагрузке стоит запуск USB-гаджета под названием “clover”. Очевидно, это что-то разработанное самой Nintendo. По лицензии GPL исходные коды модуля должны быть общедоступны. Так и есть, я нашёл его среди исходников ядра Linux на сайте Nintendo. Всего один небольшой файлик…

Судя по комментариям, его основная цель — запрос на потребление большого тока от USB. Логично, это чтобы консоль можно было питать не только от мобильных зарядок, но и от компьютера. Собственно поэтому компьютер определяет NES Mini как устройство. Что же там ещё?

Внезапно код ввода-вывода данных по USB. И код работы с псевдофайлом. Я проверил — этот псевдофайл действительно есть на NES Mini.

В этот момент меня разрывало противоречие и непонимание. С одной стороны я вижу, что это должно работать. В теории. С другой стороны я не могу поверить в то, что до этого никто не додумался раньше меня. И если это действительно так, неужели мы зря столько времени использовали такие костыли? Я быстренько набросал программу для компьютера, которая пишет данные в USB. И успешно прочитал их из этого псевдофайла. Равно как и наоборот. Это работает.

Следующие несколько дней я вспоминал свои давно забытые навыки программирования под Linux и написал демон, который работает на NES Mini и позволяет прямо по USB получить и доступ к консоли, и выполнять команды, переадресовывая при этом по USB потоки ввода-вывода. Для компьютера же написал клиент: github.com/ClusterM/clovershell-client

С его помощью сдампить всю оригинальную прошивку можно одной простой командой:

clovershell exec "dd if=/dev/mapper/root-crypt | gzip" > dump.img.gz

Позже я полностью адаптировал hakchi2 под новый метод передачи данных. В итоге для закачки игр больше не надо зажимать ресет, а передача происходит очень, очень быстро. Буквально за минуту заполнятся вся память. Раньше это занимало полчаса, причем приходилось разбивать процесс на несколько заходов, а большие файлы передать вообще не удавалось. Уж не знаю, зачем Nintendo оставила этот функционал, но это нам очень помогло.

Вскоре на основе всего этого в hakchi2 появились менеджер сохранений игр, возможность снимать скриншоты и даже FTP сервер. Да, я взял готовую библиотеку FTP-сервера, где работа с файловой системой реализовывалась в виде отдельной абстракции, и просто написал класс, который все операции работы с файлами переадресовывает по USB. В итоге подключаемся по FTP на localhost и видим файлы, которые находятся внутри NES Mini. Стоит ли говорить, насколько это облегчило работу создателям модов. UART-кабелем же даже я перестал пользоваться без особой необходимости.

Мораль сей истории такова — если ещё никто не сделал то, что кажется вам лёгким и очевидным, не останавливайтесь. Вы с высокой вероятностью действительно можете сделать это. В мире миллионы людей, которые могут делать потрясающие вещи, но не делают это только из-за того, что не верят в себя. Не верят, что это не сделали бы другие, если бы могли. А иногда достаточно просто начать.

В дальнейшем было ещё много мелких исправлений и доработок, о которых нет смысла рассказывать. Расскажу о самом интересном.

Улучшенная поддержка сторонних контроллеров

Я смог разобраться, почему многие сторонние контроллеры, разработанные для Wii, не работали с NES Mini, несмотря на одинаковый разъём и протокол. У меня таких не было, но на проблему часто жаловались. Во-первых, оказывается, что у Classic Controller’а есть два формата передачи данных. И их можно выбирать. В Интернете везде описан только один:

image

И во многих сторонних контроллерах реализован только он. Драйвер clovercon-контроллеров же (напомню, что его исходники тоже общедоступны) использует другой формат, у которого больше разрядность и точность аналоговых стиков. Весьма иронично, ведь на NES Mini изначально не используются аналоги.

Во-вторых, в коде clovercon драйвера на NES Mini есть защита от помех по питанию, которые свойственны при питании не от батареек. Он проверяет, что все неиспользуемые данные в памяти контроллера равны нулю, хотя у многих китайских контроллеров они наоборот забиты FF’ками.

Всё это решилось доработкой драйвера. Но всё равно остались очень-очень китайские контроллеры, которые не хотели работать. И проблема была аппаратная, судя по всему. Чтобы решить эту загадку, сразу двое человек прислали мне свои контроллеры для тестирования. Спасибо за это Владимиру Кондратенко и keithelmcity с сайта GBATemp. Ответ оказался прост — достаточно подтянуть линии SDA и SCL двумя резисторами к питанию. Этого требует протокол I²C, который используется в этих контроллерах. Китайцы сэкономили на парочке резисторов. Увы, исправить это не так просто, как обновить программу, но очень многие пользователи без особых проблем смогли это сделать самостоятельно. Благо, припаять два резистора сможет наверное даже тот, кто паяльник впервые держит.

Прочее

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

Воспринимается как что-то родное, и чужое одновременно.

У меня ещё оставались кое-какие идеи и планы. Например, подключить SD карту к свободным контактам, которые есть внутри NES Mini. Но я очень не люблю долго заниматься каким-то одним проектом, да и у народа интерес уже сильно угас. Так что дальнейшие изменения я наверное буду вносить только после выхода SNES Mini, если она будет взламываться.

hakchi2 на GitHub: github.com/ClusterM/hakchi2