Хабрахабр

[Перевод] Реверс-инжиниринг аркадного автомата: записываем Майкла Джордана в NBA Jam

Прошлым летом меня пригласили на тусовку в Саннивейле. Оказалось, что у хозяев в гараже есть аркадный автомат NBA JAM Tournament Edition на четверых игроков. Несмотря на то, что игре уже больше 25 лет (она была выпущена в 1993 году), в неё по-прежнему очень интересно играть, особенно для увлечённых любителей.

Согласно источникам, [1], Эм-Джей получил собственную лицензию и не был частью сделки, которую Midway заключила с NBA. Меня удивил список игроков Chicago Bulls, в котором не было Майкла Джордана.

Поэтому мне обязательно нужно было заглянуть внутрь.
Расспросив владельца автомата, я узнал, что хакеры выпустили мод игры для SNES «NBA Jam 2K17», позволяющий играть новыми игроками и Эм-Джеем, но никто не занимался разбором того, как работала аркадная версия.

Предыстория

История NBA Jam начинается не с баскетбола, а с Жан-Клод Ван Дамма. Примерно то же время, когда был выпущен «Универсальный солдат», «Midway Games» разработала технологию, позволяющую манипулировать большими оцифрованными фотореалистичными спрайтами, сохраняющими сходство с настоящими актёрами. Это был огромный технологический прорыв: анимации с 60 кадрами в секунду, невиданные ранее спрайты размером 100x100 пикселей, каждый из которых имел собственную 256-цветную палитру.

Когда переговоры закончились неудачей, Midway сменила курс и начала разработку боевой игры в духе мегахита Capcom 1991 года под названием «Street Fighter II: The World Warrior». Компания с большим успехом использовала эту технологию в популярном шутере «Terminator 2: Judgment Day»[2], но не смогла приобрести лицензию на «Универсального солдата» (финансовые условия JCVD оказались для Midway неприемлемыми [3]).

Спустя год упорного труда[4] Midway выпустила в 1992 году Mortal Kombat. Была собрана команда из четырёх человек (Эд Бун писал код, Джон Тобиас занимался артом и сценарием, Джон Вогель рисовал графику, а Дэн Форден был звукорежиссёром).

Игра с литрами крови на экране и безумно жестокими добиваниями-«фаталити» мгновенно стала мировым хитом и за год заработала почти 1 миллиард долларов[5]. Визуальный стиль сильно отличался от привычного пиксель-арта, а дизайн игры оказался, мягко говоря, «спорным».

SF2: 384×224 с 4 096 цветами.

MK: 400×254 с 32 768 цветами.

Хотя буфер кадров Mortal Kombat имеет размер 400 × 254, он растягивается до соотношения 4:3 ЭЛТ-экрана, обеспечивая разрешение 400 × 300[6] Интересный факт: как и в VGA Mode 0x13 на PC, в этих играх пиксели были не квадратными.

Оборудование Midway T-Unit

Разработанное компанией Midway для Mortal Kombat «железо» оказалось очень хорошим. Настолько хорошим, что ему дали собственное название T-Unit и повторно использовали в других играх.

  • Mortal Kombat.
  • Mortal Kombat II.
  • NBA Jam.
  • NBA Jam Tournament Edition.
  • Judge Dredd (не была выпущена).

T-Unit состоит из двух плат. Бо́льшая из них занимается игровой логикой и графикой.

Плата процессора NBA JAM TE Edition (примерно 40х40 см, или 15 дюймов).

Другая плата менее сложна, но тоже способна на многое. Она предназначена для аудио, но способна воспроизводить не только музыку при помощи FM-синтеза, но и цифровой звук.

Обратите внимание на огромный радиатор, расположенный в верхнем левом углу. Звуковая плата соединена с источником питания и графической платой, установленной сзади.

Разбираться во всём этом только на основании серийных номеров было бы очень трудоёмко. Вместе эти две платы содержат более двух сотен чипов, резисторов и EPROM. А в случае NBA Jam она оказалась просто отличной. Но, как ни удивительно, иногда у устройств родом из 90-х случайно обнаруживается документация.

Архитектура Midway T-Unit

В поисках данных я наткнулся на NBA Jam Kit. Уровень детализации этого документа потрясает[7]. Среди прочего, мне удалось найти подробное описание монтажных соединений, в том числе EPROM-ов и чипов.

Информация из документа позволила нарисовать схему плат и определить функцию каждой части. Для помощи в поиске компонентов плата имеет координаты с началом в правом нижнем углу (UA0), увеличивающиеся до левого верхнего угла (UJ26).

Сердцем основной платы служит Texas Instrument TMS34010 (UB21) с частотой 50 МГц и с 1 мебибайтом кода в EPROM-ах и 512 кибибайтами DRAM[8]. 34010 — это 32-битный чип с 16-битной шиной, имеющий такие замечательные графические инструкции, как PIXT and PIXBLT[9]. В начале 90-х этот чип использовался в нескольких картах аппаратного ускорения [10], и я думал, что он обрабатывает солидный объём графических эффектов. Как ни удивительно, но он занимается только игровой логикой, и ничего не отрисовывает.

Согласно схемам из документации, он обладает внушительными (по тем временам) 32-битной шиной данных и 32-битной адресной шиной, из-за чего стал самым большим чипом на плате. На самом деле графическим монстром оказался чип U13 под названием «DMA2». Эта специализированная интегральная схема (ASIC) способна на множество графических операций, о которых я расскажу ниже.

Мне не удалось разыскать никакой информации о протоколе шины, поэтому если вам что-то о нём известно, пишите на электронную почту. Все чипы (System RAM, GFX EPROM, Palette SDRAM, Code, Video Banks) отображены в одно 32-битное адресное пространство и подключены к одной шине.

Эти EPROM на 512 кибибайта имеют 32-битные адресные выводы и 8-битные выводы данных. Обратите на хитрый трюк: один компонент EPROM (отмечен синим) используется для создания другой системы хранения (и экономии денег). Аналогичным образом графические ресурсы подключены с четырёхкратным чередованием адресов для образования 32-битного адреса с 32-битной системой хранения данных, содержащей 8 мебибайт. Для 34010, которому требуется 16-битная шина данных, два EPROM (J12 и G12) подключены с двукратным чередованием адресов, создавая память в 1 мебибайт.

Хотя в этой статье я в основном буду рассматривать графический конвейер, не могу противиться искушению, а потому вкратце расскажу про аудиосистему.

Схеме звуковой карты показан Motorola 6809 (U4 с частотой 2 МГц), на который подаются инструкции из одного EPROM (U3) для управления музыкой и звуковыми эффектами.

Чип FM-синтеза Yamaha 2151 (3,5 МГц) генерирует музыку непосредственно из инструкций, полученных от 6809 (музыка использует довольно малую полосу пропускания).

OKI6295 (1 МГц) отвечает за воспроизведение цифрового аудио в формате ADPCM (например, легендарной «Boomshakalaka»[11] Тима Китцроу).

Заметьте, что на основной плате те же синие 512-кибибайтные EPROM 32a/8d используются в 16-битной системе с двукратным чередованием адресов для хранения оцифрованных голосов, а для 8-битных инструкций данных/адресов Motorola 6809 чередования нет.

Жизнь кадра

Весь экран NBA Jam индексирован в 16-битной палитре. Цвета хранятся в формате xRGB 1555 в палитре размером 64 кибибайт. Палитра разделена на 128 блоков (256 * 16 бит) по 512 байт. Спрайты, хранящиеся в EPROM, помечены как «GFX». Каждый спрайт имеет собственную палитру размером до 256x16-битных цветов. Спрайт часто использует целый блок палитры, но никогда не больше одного. ЭЛТ-сигнал передаётся на монитор при помощи RAMDAC, который для каждого пикселя считывает индекс из банков Video DRAM и выполняет поиск цвета в палитре.

Жизнь каждого кадра видео NBA Jam протекает следующим образом:

  1. Игровая логика состоит из потока 16-битных инструкций, передаваемых из J12/G12 в 34010.
  2. 34010 считывает ввод игроков, вычисляет состояние игры, а затем отрисовывает экран.
  3. Для отрисовки на экране 34010 сначала находит неиспользуемый блок в палитре и записывает туда палитру спрайта (палитры спрайтов хранятся вместе с инструкциями 34010 в J12/G12).
  4. 34010 выполняет запрос к DMA2, в который включаются адрес и размеры спрайта, используемый 8-битный блок палитры, усечение, масштабирование, способ обработки прозрачных пикселей, и так далее.
  5. DMA2 считывает 8-битные индексы спрайтов из GFX ROM чипа J14-G23, комбинирует это значение с индексом 8-битного блока палитры и записывает 16-битный индекс в видеобанки. DRAM2 можно считать блиттером, считывающим 8-битные значения из GFX EPROM и записывающим 16-битные значения в видеобанки
  6. Шаги 3-5 повторяются, пока не будут выполнены все запросы на отрисовку спрайтов.
  7. Когда наступает момент обновления экрана, RAMDAC преобразует находящиеся в видеобанках данные в сигнал, который может понять ЭЛТ-монитор. Чтобы полосы пропускания хватило на преобразование 16-битного индекса в 16-битный RGB, палитра хранится в чрезвычайно дорогой и чрезвычайно быстрой SRAM.

Интересный факт: флеш-прошивка EPROM — это не такой уж простой процесс. Перед записью в чип необходимо полностью стереть всё его содержимое.

Для начала нужно отклеить стикер с верхней части EPROM, чтобы открыть его схему. Для этого чип необходимо облучить УФ-освещением. Затем EPROM помещается в особое устройство-стиратель, в котором есть УФ-лампа.

Спустя 20 минут EPROM будет заполнен нулями и готов к записи.

Документация MAME

Разобравшись с оборудованием, я понял, в какой набор EPROM можно было записaть Майкла Джордана (палитра хранится в Code EPROM-ах, а индексы — в GFX EPROM-ах). Однако я по-прежнему не знал ни точного местоположения, ни используемого формата.

Недостающая документация нашлась в MAME.

MAME построена на основе концепции «драйверов», являющихся имитацией платы. На случай, если вы не знаете, как работает этот потрясающий эмулятор, вкратце объясню. В случае Midway T-Unit нас интересуют следующие файлы: Каждый драйвер составлен из компонентов, имитирующих (обычно) каждый чип.

mame/includes/midtunit.h
mame/src/mame/video/midtunit.cpp
mame/src/mame/drivers/midtunit.cpp
mame/src/mame/machine/midtunit.cpp
cpu/tms34010/tms34010.h

Если взглянуть на drivers/midtunit.cpp, то мы увидим, что каждый чип памяти является частью единого 32-битного адресного пространства. Из исходного кода драйвера видно, что палитра начинается с адреса 0x01800000, gfxrom — с адреса 0x02000000, а чип DMA2 — с 0x01a80000. Чтобы проследовать по пути данных, нам нужно проследить за функциями C++, выполняемыми, когда объектом операции считывания или записи является адрес памяти.

void midtunit_state::main_map(address_map &map) { map.unmap_value_high(); map(0x00000000, 0x003fffff).rw(m_video, FUNC(midtunit_vram_r), FUNC(midtunit_vram_w)); map(0x01000000, 0x013fffff).ram(); map(0x01400000, 0x0141ffff).rw(FUNC(midtunit_cmos_r), FUNC(midtunit_cmos_w)).share("nvram"); map(0x01480000, 0x014fffff).w(FUNC(midtunit_cmos_enable_w)); map(0x01600000, 0x0160000f).portr("IN0"); map(0x01600010, 0x0160001f).portr("IN1"); map(0x01600020, 0x0160002f).portr("IN2"); map(0x01600030, 0x0160003f).portr("DSW"); map(0x01800000, 0x0187ffff).ram().w(m_palette, FUNC(write16)).share("palette"); map(0x01a80000, 0x01a800ff).rw(m_video, FUNC(midtunit_dma_r), FUNC(midtunit_dma_w)); map(0x01b00000, 0x01b0001f).w(m_video, FUNC(midtunit_control_w)); map(0x01d00000, 0x01d0001f).r(FUNC(midtunit_sound_state_r)); map(0x01d01020, 0x01d0103f).rw(FUNC(midtunit_sound_r), FUNC(midtunit_sound_w)); map(0x01d81060, 0x01d8107f).w("watchdog", FUNC(watchdog_timer_device::reset16_w)); map(0x01f00000, 0x01f0001f).w(m_video, FUNC(midtunit_control_w)); map(0x02000000, 0x07ffffff).r(m_video, FUNC(midtunit_gfxrom_r)).share("gfxrom"); map(0x1f800000, 0x1fffffff).rom().region("maincpu", 0); /* mirror used by MK*/ map(0xff800000, 0xffffffff).rom().region("maincpu", 0);
}

В конце того же файла «drivers/midtunit.cpp» мы видим, как содержимое EPROM-ов загружается в ОЗУ. В случае графических ресурсов «gfxrom» (сопоставленных с адресом 0x02000000), мы можем увидеть, что они растянулись на 8 мебибайта адресного пространства в блоках чипов с четырёхкратным чередованием адресов. Заметьте, что имена файлов соответствуют расположению чипов (например, UJ12/UG12). Набор этих файлов EPROM в мире эмуляторов более известен под названием «ROM».

ROM_START( nbajamte ) ROM_REGION( 0x50000, "adpcm:cpu", 0 ) /* sound CPU*/ ROM_LOAD( "l1_nba_jam_tournament_u3_sound_rom.u3", 0x010000, 0x20000, NO_DUMP) ROM_RELOAD( 0x030000, 0x20000 ) ROM_REGION( 0x100000, "adpcm:oki", 0 ) /* ADPCM*/ ROM_LOAD( "l1_nba_jam_tournament_u12_sound_rom.u12", 0x000000, 0x80000, NO_DUMP) ROM_LOAD( "l1_nba_jam_tournament_u13_sound_rom.u13", 0x080000, 0x80000, NO_DUMP) ROM_REGION16_LE( 0x100000, "maincpu", 0 ) /* 34010 code*/ ROM_LOAD16_BYTE( "l4_nba_jam_tournament_game_rom_uj12.uj12", 0x00000, 0x80000, NO_DUMP) ROM_LOAD16_BYTE( "l4_nba_jam_tournament_game_rom_ug12.ug12", 0x00001, 0x80000, NO_DUMP) ROM_REGION( 0xc00000, "gfxrom", 0 ) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_ug14.ug14", 0x000000, 0x80000, NO_DUMP) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_uj14.uj14", 0x000001, 0x80000, NO_DUMP) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_ug19.ug19", 0x000002, 0x80000, NO_DUMP) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_uj19.uj19", 0x000003, 0x80000, NO_DUMP) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_ug16.ug16", 0x200000, 0x80000, NO_DUMP) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_uj16.uj16", 0x200001, 0x80000, NO_DUMP) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_ug20.ug20", 0x200002, 0x80000, NO_DUMP) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_uj20.uj20", 0x200003, 0x80000, NO_DUMP) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_ug17.ug17", 0x400000, 0x80000, NO_DUMP) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_uj17.uj17", 0x400001, 0x80000, NO_DUMP) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_ug22.ug22", 0x400002, 0x80000, NO_DUMP) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_uj22.uj22", 0x400003, 0x80000, NO_DUMP) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_ug18.ug18", 0x600000, 0x80000, NO_DUMP) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_uj18.uj18", 0x600001, 0x80000, NO_DUMP) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_ug23.ug23", 0x600002, 0x80000, NO_DUMP) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_uj23.uj23", 0x600003, 0x80000, NO_DUMP)
ROM_END

Интересный факт: в показанном выше примере кода последний параметр функции был заменён на «NO_DUMP», чтобы можно было загружать модифицированные EPROM. Эти поля обычно[12] являются хешем CRC/SHA1 содержимого EPROM. Именно так MAME определяет, какой игре принадлежит ROM и позволяет узнать, что один из ROM-ов в наборе отсутствует или повреждён.

Сердце видеодвижка: DMA2

Ключом к пониманию формата графики является функция, обрабатывающая запись/чтение DMA в 256 регистров DMA2, расположенные по адресам с 0x01a80000 до 0x01a800ff. Весь тяжкий труд по обратной разработке уже был выполнен разработчиками MAME. Они даже уделили время превосходному документированию формата команд.

Регистры DMA ------------------ Регистр | Бит | Применение ----------+-FEDCBA9876543210-+------------ 0 | xxxxxxxx-------- | пиксели, отбрасываемые в начале каждой строки | --------xxxxxxxx | пиксели, отбрасываемые в конце каждой строки 1 | x--------------- | включение записи (или очистки, если ноль) | -421------------ | bpp изображения (0=8) | ----84---------- | размер пропуска после = (1<<x) | ------21-------- | размер пропуска до = (1<<x) | --------8------- | включение пропуска до/после | ---------4------ | включение усечения | ----------2----- | отзеркаливание по y | -----------1---- | отзеркаливание по x | ------------8--- | передача ненулевых пикселей как цвета | -------------4-- | передача нулевых пикселей как цвета | --------------2- | передача ненулевых пикселей | ---------------1 | передача нулевых пикселей 2 | xxxxxxxxxxxxxxxx | младшее слово адреса исходника 3 | xxxxxxxxxxxxxxxx | старшее слово адреса исходника 4 | -------xxxxxxxxx | x получателя 5 | -------xxxxxxxxx | y получателя 6 | ------xxxxxxxxxx | столбцы изображения 7 | ------xxxxxxxxxx | строки изображения 8 | xxxxxxxxxxxxxxxx | палитра 9 | xxxxxxxxxxxxxxxx | цвет 10 | ---xxxxxxxxxxxxx | масштаб по x 11 | ---xxxxxxxxxxxxx | масштаб по y 12 | -------xxxxxxxxx | усечение сверху/слева 13 | -------xxxxxxxxx | усечение снизу/справа 14 | ---------------- | тест 15 | xxxxxxxx-------- | байт обнаружения нуля | --------8------- | дополнительная страница | ---------4------ | размер получателя | ----------2----- | выбор верха/низа или левого/правого края для регистра 12/13

Существует даже функция отладки, позволяющая сохранять исходные спрайты в процессе передачи их DMA2 (функция написана давним участником проекта MAME Райаном Холтцом[13]). Мне достаточно было просто сыграть в игру, чтобы все файлы с метаданными сохранились на диск.

Однако не у всех спрайтов количество цветов одинаково. Оказалось, что спрайты составлены из простых элементов 16-битной палитры без сжатия. Некоторые спрайты используют только 16 цветов с 4-битными индексами цветов, а другие — 256 цветов и требуют 8-битных индексов цветов.

Патчинг

Теперь я знаю расположение и формат спрайтов, поэтому осталось выполнить минимальный объём реверс-инжиниринга. Я написал на Golang небольшую программу для устранения чередования EPROM-ов «code» и «gfx». Устранив чередование, легко выполнять поиск ASCII или известных значений, потому что я работал ровно с тем, как выглядит ОЗУ во время выполнения программы.

Оказалось, что все они хранились один за другим в 16-битном беззнаковом формате big-endian (что очень логично, ведь 34010 работает с big-endian). После этого легко можно найти характеристики игрока. Не особо разбираясь в баскетболе, я ввёл SPEED=9, 3 PTS=9, DUNKS=9, PASS=9, POWER=9, STEAL=9, BLOCK=9 и CLTCH=9. Я добавил патчер для модификации атрибутов игроков.

Для фотографии Эм-Джея я создал 256-цветный индексированный PNG (его можно посмотреть здесь). Также я написал код для патчинга игры новыми спрайтами с единственным ограничением — новые спрайты должны иметь те же размеры, что и заменяемые.

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

Запускаем игру

После патчинга содержимого EPROM инструмент диагностики NBAJam показал, что содержимое некоторых чипов помечено как «BAD». Я этого ожидал, потому что пропатчил только содержимое EPROM-ов, но не озаботился поиском формата CRC и даже местом их хранения.

Два EPROM-а, в которых хранятся инструкции (UG12 и UJ12) тоже красные, потому что там находятся палитры. GFX EPROM-ы помечены красным (UG16/UJ16, UG17/UJ17, UG18/UJ18, UG20/UJ20, UG22/UJ22 и UG23/UJ23), потому что в них хранятся изменённые мной изображения.

Игра запустилась. К счастью, здесь CRC не используются для защиты от модифицированного контента и нужны только для проверки целостности чипов. И заработала!

Hasta La Vista, Baby!

Закончив с техническими трудностями, я быстро потерял интерес к инструменту и прекратил его разработку. Идеи для тех, кто захочет поиграться с кодом:

  • Добавьте в Восточную конференцию Toronto Raptors.
  • Добавьте возможность изменения имён игроков. К сожалению, они состоят не из ASCII, а являются заранее сгенерированными изображениями.

Книга про NBA Jam

Если вы фанат NBA Jam, то Рейан Али написал о ней целую книгу[14]. Купить её можно здесь.

Исходный код

Если вы хотите внести свой вклад или просто посмотреть, как всё устроено, то полный исходный выложен на github здесь.

Ссылки

[1] Источник: 'NJA Jam' by Reyan Ali

[2] Источник: 'NJA Jam' by Reyan Ali

[3] Источник: 'NJA Jam' by Reyan Ali

[4] Источник: Mortal Kombat 1 Behind The Scenes

[5] Источник: 'NJA Jam' by Reyan Ali

[6] Источник: 4:3 versus Square Pixels

[7] Комментарий: к сожалению, эпоха такой великолепной документации давно прошла

[8] Источник: Mame NBA Jam start-up screen

[9] Источник: TMS34010 Instruction Set

[10] Источник: T34010 User Guide

[11] Источник: NBA Jam—BoomShakaLaka video

[12] Источник: MAME T-Unit driver.cpp

[13] Источник: Commit 'midtunit.cpp: Added an optional DMA-blitter viewer'

[14] Источник: 'NBA JAM Book' by Reyan Ali

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть