Этот пост посвящён тому, как я портировал 8-битный Sonic 2 на TI-84+ CE
Часть 1: с чего всё началось
Эта история началась осенью 2022 года, когда я смотрел видео This Does Not Compute об истории игр на графических калькуляторах. Примерно на пятой минуте автор мимоходом упомянул типы процессоров, которые использовались в этой линейке графических калькуляторов. В большинстве из них был установлен Z80, в 89 и 92 задействовали M68K, а в линейке Nspire использован процессор на основе ARM.
Это меня очень заинтересовало, ведь я знал, какие процессоры использовала Sega в своих ретроконсолях: Z80 в Master System и M68K в Genesis. Калькуляторы имеют экраны в градациях серого, но мне захотелось узнать, пробовал ли кто-нибудь портировать игру Sonic с консолей на один из калькуляторов.
К своему удивлению, я выяснил, что этого никто не делал; более того, за исключением SonicUP и Sonic TI-Blast никто не создавал завершённый платформер про Соника для калькуляторов TI. Если не считать Nspire, но он настолько мощный, что способен эмулировать старые игровые консоли, так что меня он не особо интересует.
Поэтому я задался вопросом: удастся ли мне портировать уже существующий дизассемблированный код Sonic на графический калькулятор TI. Но сначала мне нужно было выбрать конкретную игру.
Часть 2: выбор игры
Во-первых, я должен был выбрать конкретный калькулятор, на который я буду портировать игру про Соника. В этом не было ничего сложного. Вся линейка 83+, за исключением самых новых моделей, 84+ CSE и CE, имела дисплей в градациях серого с разрешением 96x64. Линейка на процессоре M68K имела экран 160x140. Консоль Sega с наименьшим разрешением (Game Gear) обладала цветным дисплеем 160x144. Genesis, ближайший аналог линейки TI-68K, имела разрешение не менее 320x224.
Это значит, что у нас остаются CSE и CE. Но постойте! CSE печально известен своей тормознутостью из-за использования ровно того же процессора Z80, имея при этом 16-битный дисплей разрешением 320x240! Похоже, нам остаётся только CE.
Итак, TI-84+ CE обладает процессором eZ80 с частотой 48 МГц (по сути, 24-битным Z80) с 256 КБ ОЗУ и дисплеем 320x240. Это может показаться впечатляющим, но от систем Sega CE отличался следующим:
- Полным отсутствием какого-либо графического оборудования.
- Состояниями ожидания, снижавшими действительную тактовую частоту примерно до 12-20 МГц.
- Форматом файлов, ограничивающим размер программы до 64 КБ, чего слишком мало для хранения любой игры Sonic без разбиения её на несколько файлов.
Из-за всех этих факторов портирование любой из игр для Genesis превратилось бы в настоящее мучение. Вот если бы существовала консоль Sega с той же архитектурой, что и у CE…
О, постойте, так ведь она же есть: Sega Master System. С процессором Z80 на 3,5 МГц, графическим процессором SEGA-VDP, 8 КБ ОЗУ и 16 КБ VRAM. Похоже, она идеально подходит для моего небольшого проекта.
Теперь нужно найти игру про Соника, которую можно легко модифицировать. Благодаря ребятам из Sonic Retro существует множество полнофункциональных дизассемблированных игр Sonic.
К сожалению, у них есть единственная игра для Master System Sonic — 8-битный Sonic 2. Это милая небольшая игра 1992 года, вышедшая на месяц раньше версии для Genesis.
Выбрав целевую платформу и игру для портирования, можно было приступать к модифицированию игры, чтобы она запускалась на 84+ CE.
Часть 3: замена ассемблера
Целевой платформой дизассемблированного кода 8-битного Sonic 2 является WLA-DX — ассемблер общего назначения для множества ретроархитектур. Он хорошо подходит для нужд Sonic Retro, но не поддерживает eZ80. Значит, мне нужно заменить ассемблер.
Я выбрал SPASM-ng — ассемблер Z80/eZ80, специально созданный для калькуляторов TI. Это означает, что мне не придётся особо заморачиваться, чтобы превратить двоичный файл в программу или файл данных TI-84+ CE.
Кроме того, у него есть множество директив, очень похожих на директивы WLA-DX.
Вот как может выглядеть небольшая программа на ассемблере для WLA-DX в сравнении с программой для SPASM-ng:
;WLA-DX example
.include "defines.asm"
KillTails:
ld a, ~$00
dec l
jr nz, ++
inc a
jr +
++: add a, l
ld a, l
ld a, $20
add a, h
ld h, a
+: ret
;SPASM-ng example
#include "defines.asm"
KillTails:
ld a, $FF
dec l
jr nz, +_
inc a
jr ++_
_: add a, l
ld a, l
ld a, $20
add a, h
ld h, a
_: ret
Как видите, некоторые части, например, локальные метки и include, одинаковы между ассемблерами, другие вещи, например, макрос отрицания (~), отличаются.
А теперь давайте возьмём этот пример из 11 строк и применим сделанное мной к двадцати тысячам строк, составляющих дизассемблированный код. Кажется, работа предстоит большая, правда?
В конечном итоге у меня на это ушло очень много времени. Но закончив, я наконец мог приступить к подключению прерываний и палитр к их аналогам в 84+ CE.
Часть 4: общие различия в оборудовании
На этом этапе игра могла ассемблироваться в файл 8XP размером 32 КБ. Разумеется, она вылетала, причиной чего была одна серьёзная проблема:
TI-84+ CE намеренно изготовлен таким образом, чтобы выполнять сброс при любом доступе ввода-вывода.
У Master System довольно многое привязано к портам ввода-вывода: ввод, прерывания, палитры, VRAM и звук. У 84+ CE отсутствуют динамики, так что я просто удалил весь код, связанный с аудио.
К счастью, TI не настолько глупый — разработчики добавили в CE ввод-вывод с отображением в память. Он охватывает всё перечисленное мной, так что можно было просто сменить все команды ввода-вывода из дизассемблированного кода на их аналоги с отображением в память. Мне оставалось только добавить несколько процедур, чтобы всё выглядело одинаково.
▍ Палитры
Начнём с палитр. Master System имеет 6-битную цветовую палитру, в которой одновременно доступны 32 из 64 цветов.
Палитра Master System, взятая с SMS Power!
По умолчанию TI-84+ CE вообще не имеет палитры, но ручная настройка ЖК-дисплея позволяет получить доступ к режиму с 16-битной цветовой палитрой, где одновременно доступны 256 из 65536 цветов. Это намного больше, чем даже у Genesis, так что мне оставалось только добавить таблицу данных для каждого цвета в палитре Master System.
Но изначально я поступил не так. Я ошибочно понял документацию в WikiTI, поэтому думал, что это единственная палитра, которую можно использовать в 8-битном режиме:
Палитра, взятая из тулчейна языка C 84+ CE
Я осознал свою ошибку, только когда уже преобразовал все палитры в игре для работы в показанной выше. Что ж…
▍ Ввод с контроллера
Далее нужно разобраться со вводом: нажатиями на кнопки, кнопкой паузы и кнопкой сброса.
Master System хранит всю информацию о вводе в одной 16-битной битовой маске. Мы можем игнорировать один из байтов, потому что он используется только в играх на двух игроков. Кнопка паузы не относится к этой битовой маске, потому что она запускает немаскируемое прерывание.
У 84+ CE не восемь кнопок, а побольше, поэтому он использует 56-битную битовую маску. Чаще всего для игр используют клавиши со стрелками, 2nd и Alpha, поэтому я привязал к ним D-Pad и кнопки 1/2. Так как кнопка паузы работает не так, как все остальные, чтобы заставить её работать, мне пришлось добавить кое-что в обработчик ввода.
▍ Работа с банками ROM
Master System имеет адресное пространство на 64 КБ, а 8-битный Sonic 2 имеет размер 512 КБ. Что нам с этим делать?
Здесь нам на помощь приходит работа с банками ROM. В картридже существует специальный чип, называемый мэппером, сопоставляющий части ROM с таблицей распределения памяти CPU. Каждая часть ROM называется банком; у 8-битного Sonic 2 их 31, если не считать банк 0, который загружен постоянно.
Банки 2 и 3 связаны со звуком, поэтому я просто удалил их. Осталось 29 банков, а именно:
- Те, которые должны занимать одно и то же место в памяти.
- Слишком многочисленные, чтобы уместиться в ОЗУ за раз или совместно с банком 0.
- В переменной __cpLocations во флеш-памяти, что не позволяет мне просто перейти к месту в коде, не загрузив его предварительно куда-нибудь.
То есть единственный способ заставить их работать — воспользоваться процедурами управления данных TI для нахождения банков во флеш-памяти и копирования их в одно и то же место в ОЗУ каждый раз, когда они необходимы.
Сразу понятно, что это очень медленный процесс. Крайне медленный. Мне хотелось бы сказать, что я нашёл полное решение, но это не так.
Лучшее, что я смог сделать — это загрузить самое важное, хранящееся в банках, например, данные коллизий, в 16-битные интервалы $E000-$FFFF, которые являются в Master System зеркально отражённой памятью, а в CE остаются ОЗУ, которое можно использовать.
▍ Различные функции VDP
Наконец, нам нужно реализовать функции VDP, например, прерывания и доступ к VRAM.
С прерываниями всё просто.
В Master System VDP запускает прерывание CPU каждый раз, когда завершает отрисовку на телевизоре, то есть 60 раз в секунду.
В 84+ CE ЖК-экран можно настроить так, чтобы прерывание срабатывало каждый раз, когда он завершит отрисовку кадра. То есть 60 раз в секунду.
Реализовать доступ к VRAM в 84+ CE просто. VRAM — это обычная ОЗУ. В Master System всё чуть сложнее. Два байта передаются через ввод-вывод на VDP. Эти байты используются как адрес, потому можно побайтово передавать данные через ввод-вывод в VRAM.
По сути, я заменил все OUT на LDIR, так как каждый доступ к VRAM в игре предназначался для передачи больших блоков данных.
После того, как я всё это сделал, игра начала идеально работать!
Как выглядела заставка на этом этапе
Да, я вас обманул. Помните, я говорил, что в 84+ CE нет никакого графического оборудования? Это значит, что рендеринг не выполняется. В заставке отображается только цвет фона, который обрабатывается палитрами.
Это значит, что для завершения проекта нужно сделать только одно: создать рендерер!
Часть 5: создание рендерера
Давайте начнём с объяснения того, как Master System и CE рендерят кадр.
В CE есть буфер кадров с глубиной 8 битов на пиксель, занимающий 75 КБ. Ни больше, ни меньше. В Master System есть два отрендеренных слоя — фон (или Screen Map) и Sprite Attribute Table (SAT).
Screen Map содержит 896 16-битных битовых масок, каждая из которых создаёт сетку тайлов на экране. В этой битовой маске есть 9 битов для выбора того, какой тайл отрисовывать, и по 1 биту на зеркальное отражение по горизонтали и вертикали, индекс используемой палитры и выбор того, нужно ли выполнять отрисовку поверх спрайтов.
Два регистра VDP используется для скроллинга. Это единственные регистры VDP, которые игра активно использует, поэтому я могу делать допущения о конфигурации VDP, не сталкиваясь с особо серьёзными багами.
У Master System есть 64 спрайта, а SAT состоит из 256-байтового блока; три байта выделены под позицию по X, позицию по Y и под выбранный тайл. Позиции по Y занимают первые 64 бита, а позиция по X и выбранный тайл занимают в шахматном порядке последние 128 битов.
Первым делом я написал рендерер для слоя SAT, потому что его довольно легко воссоздать. Так как в eZ80 появилась новая команда для 16-битного умножения (MLT), я мог намного быстрее вычислять такие вещи, как позиция спрайта и местонахождение тайла во VRAM.
Вот как выглядела игра после того, как я реализовал рендеринг SAT.
После этого настало время создания рендерера для Screen Map. Учитывая, что в ней хранятся аж 896 тайлов, я не мог просто конвертировать тайлы на лету, как в случае спрайтов, а из-за регистров скроллинга я не мог просто загрузить их все на экран. По крайней мере, как сырое изображение.
Моё решение было таким:
- Выделить буфер кадров на 56 КБ во VRAM калькулятора CE под хранение сырой Screen Map, конвертированной под глубину цвета 8 битов на пиксель, что позволило правильно реализовать скроллинг карты тайлов.
- Хранить кэш конвертированных тайлов фона в обычной ОЗУ, что в восемь раз снизило количество тактов, необходимое для их отрисовки в буфер кадров.
Это позволило реализовать очень быстрый рендеринг Screen Map, который вы можете видеть ниже:
Также я добавил Spin Dash. Спасибо, pixelcat!
Часть 6: несколько дополнительных функций
После воссоздания всех важных функций Master System (за исключением тайлов с приоритетом) порт, по сути, был завершён! Оставалось устранить ещё несколько багов, которые я внёс, когда вручную редактировал почти тысячу локальных меток.
Также я добавил экран заставки SEGA. На самом деле её не было в игре. Логотип SEGA хранится в BIOS Master System и отображается им.
Я создал этот экран с нуля, воспользовавшись теми же инструментами тулчейна языка C для CE, о которых говорил ранее. Это не то же изображение, которое использовалось на Master System/Game Gear, я взял его из Sonic 3.
Сделав всё это, я мог с полным правом сказать, что первым реализовал Sonic на линейке TI-84+. Он неидеален, так что если вы очень хотите сыграть в Sonic на графическом калькуляторе, то лучше купите Nspire.
Telegram-канал со скидками, розыгрышами призов и новостями IT ?