[Перевод] Реверс-инжиниринг аркадного автомата: записываем Майкла Джордана в NBA Jam
Прошлым летом меня пригласили на тусовку в Саннивейле. Оказалось, что у хозяев в гараже есть аркадный автомат NBA JAM Tournament Edition на четверых игроков. Несмотря на то, что игре уже больше 25 лет (она была выпущена в 1993 году), в неё по-прежнему очень интересно играть, особенно для увлечённых любителей.
Меня удивил список игроков Chicago Bulls, в котором не было Майкла Джордана. Согласно источникам, [1], Эм-Джей получил собственную лицензию и не был частью сделки, которую Midway заключила с NBA.
Расспросив владельца автомата, я узнал, что хакеры выпустили мод игры для SNES «NBA Jam 2K17», позволяющий играть новыми игроками и Эм-Джеем, но никто не занимался разбором того, как работала аркадная версия. Поэтому мне обязательно нужно было заглянуть внутрь.
Предыстория
История NBA Jam начинается не с баскетбола, а с Жан-Клод Ван Дамма. Примерно то же время, когда был выпущен «Универсальный солдат», «Midway Games» разработала технологию, позволяющую манипулировать большими оцифрованными фотореалистичными спрайтами, сохраняющими сходство с настоящими актёрами. Это был огромный технологический прорыв: анимации с 60 кадрами в секунду, невиданные ранее спрайты размером 100×100 пикселей, каждый из которых имел собственную 256-цветную палитру.
Компания с большим успехом использовала эту технологию в популярном шутере «Terminator 2: Judgment Day»[2], но не смогла приобрести лицензию на «Универсального солдата» (финансовые условия JCVD оказались для Midway неприемлемыми [3]). Когда переговоры закончились неудачей, Midway сменила курс и начала разработку боевой игры в духе мегахита Capcom 1991 года под названием «Street Fighter II: The World Warrior».
Была собрана команда из четырёх человек (Эд Бун писал код, Джон Тобиас занимался артом и сценарием, Джон Вогель рисовал графику, а Дэн Форден был звукорежиссёром). Спустя год упорного труда[4] Midway выпустила в 1992 году Mortal Kombat.
Визуальный стиль сильно отличался от привычного пиксель-арта, а дизайн игры оказался, мягко говоря, «спорным». Игра с литрами крови на экране и безумно жестокими добиваниями-«фаталити» мгновенно стала мировым хитом и за год заработала почти 1 миллиард долларов[5].
SF2: 384×224 с 4 096 цветами.
MK: 400×254 с 32 768 цветами.
Интересный факт: как и в VGA Mode 0×13 на PC, в этих играх пиксели были не квадратными. Хотя буфер кадров Mortal Kombat имеет размер 400 × 254, он растягивается до соотношения 4:3 ЭЛТ-экрана, обеспечивая разрешение 400 × 300[6]
Оборудование 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. Разбираться во всём этом только на основании серийных номеров было бы очень трудоёмко. Но, как ни удивительно, иногда у устройств родом из 90-х случайно обнаруживается документация. А в случае NBA Jam она оказалась просто отличной.
Архитектура 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], и я думал, что он обрабатывает солидный объём графических эффектов. Как ни удивительно, но он занимается только игровой логикой, и ничего не отрисовывает.
На самом деле графическим монстром оказался чип U13 под названием «DMA2». Согласно схемам из документации, он обладает внушительными (по тем временам) 32-битной шиной данных и 32-битной адресной шиной, из-за чего стал самым большим чипом на плате. Эта специализированная интегральная схема (ASIC) способна на множество графических операций, о которых я расскажу ниже.
Все чипы (System RAM, GFX EPROM, Palette SDRAM, Code, Video Banks) отображены в одно 32-битное адресное пространство и подключены к одной шине. Мне не удалось разыскать никакой информации о протоколе шины, поэтому если вам что-то о нём известно, пишите на электронную почту.
Обратите на хитрый трюк: один компонент EPROM (отмечен синим) используется для создания другой системы хранения (и экономии денег). Эти EPROM на 512 кибибайта имеют 32-битные адресные выводы и 8-битные выводы данных. Для 34010, которому требуется 16-битная шина данных, два EPROM (J12 и G12) подключены с двукратным чередованием адресов, создавая память в 1 мебибайт. Аналогичным образом графические ресурсы подключены с четырёхкратным чередованием адресов для образования 32-битного адреса с 32-битной системой хранения данных, содержащей 8 мебибайт.
Хотя в этой статье я в основном буду рассматривать графический конвейер, не могу противиться искушению, а потому вкратце расскажу про аудиосистему.
Схеме звуковой карты показан 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». Каждый спрайт имеет собственную палитру размером до 256×16-битных цветов. Спрайт часто использует целый блок палитры, но никогда не больше одного. ЭЛТ-сигнал передаётся на монитор при помощи RAMDAC, который для каждого пикселя считывает индекс из банков Video DRAM и выполняет поиск цвета в палитре.
Жизнь каждого кадра видео NBA Jam протекает следующим образом:
- Игровая логика состоит из потока 16-битных инструкций, передаваемых из J12/G12 в 34010.
- 34010 считывает ввод игроков, вычисляет состояние игры, а затем отрисовывает экран.
- Для отрисовки на экране 34010 сначала находит неиспользуемый блок в палитре и записывает туда палитру спрайта (палитры спрайтов хранятся вместе с инструкциями 34010 в J12/G12).
- 34010 выполняет запрос к DMA2, в который включаются адрес и размеры спрайта, используемый 8-битный блок палитры, усечение, масштабирование, способ обработки прозрачных пикселей, и так далее.
- DMA2 считывает 8-битные индексы спрайтов из GFX ROM чипа J14-G23, комбинирует это значение с индексом 8-битного блока палитры и записывает 16-битный индекс в видеобанки. DRAM2 можно считать блиттером, считывающим 8-битные значения из GFX EPROM и записывающим 16-битные значения в видеобанки
- Шаги 3–5 повторяются, пока не будут выполнены все запросы на отрисовку спрайтов.
- Когда наступает момент обновления экрана, 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-битного адресного пространства. Из исходного кода драйвера видно, что палитра начинается с адреса 0×01800000, gfxrom — с адреса 0×02000000, а чип DMA2 — с 0×01a80000. Чтобы проследовать по пути данных, нам нужно проследить за функциями 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» (сопоставленных с адресом 0×02000000), мы можем увидеть, что они растянулись на 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, расположенные по адресам с 0×01a80000 до 0×01a800ff. Весь тяжкий труд по обратной разработке уже был выполнен разработчиками MAME. Они даже уделили время превосходному документированию формата команд.
Регистры DMA ------------------ Регистр | Бит | Применение ----------+-FEDCBA9876543210-+------------ 0 | xxxxxxxx-------- | пиксели, отбрасываемые в начале каждой строки | --------xxxxxxxx | пиксели, отбрасываемые в конце каждой строки 1 | x--------------- | включение записи (или очистки, если ноль) | -421------------ | bpp изображения (0=8) | ----84---------- | размер пропуска после = (1<
Существует даже функция отладки, позволяющая сохранять исходные спрайты в процессе передачи их 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 и даже местом их хранения.GFX EPROM-ы помечены красным (UG16/UJ16, UG17/UJ17, UG18/UJ18, UG20/UJ20, UG22/UJ22 и UG23/UJ23), потому что в них хранятся изменённые мной изображения. Два EPROM-а, в которых хранятся инструкции (UG12 и UJ12) тоже красные, потому что там находятся палитры.
К счастью, здесь 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