Два Z80 на одну машину: чем отличались 8-битные аркадные автоматы от домашних компьютеров?

в 8:48, , рубрики: mame, Z80, аркадные автоматы, восьмибитные компьютеры, Игры и игровые приставки, старое железо, эмуляторы

Что я узнал об аркадном автомате Bomb Jack в процессе создания его эмулятора

Два Z80 на одну машину: чем отличались 8-битные аркадные автоматы от домашних компьютеров? - 1

Недавно я написал небольшой эмулятор автомата Bomb Jack, в основном для того, чтобы разобраться, чем эти первые 8-битные аркадные автоматы отличались по конструкции от 8-битных домашних компьютеров.

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

Сначала я думал над идеей создания эмулятора Pengo, потому что мой подростковый мозг эта игра впечатлила гораздо сильнее, чем Bomb Jack (кстати, вот моя карго-культовая версия Pengo). Но аркадное оборудование Pengo потребовало бы создания эмуляторов новых чипов для звука и видео, а для Bomb Jack оказалось достаточно уже имевшихся у меня частей (Z80 в качестве ЦП и AY-3-8910 для звука), поэтому первым я взялся за Bomb Jack.

Кроме того, Bomb Jack стал отличной возможности наконец-то добавить поддержку NMI (non-maskable interrupt) в мой эмулятор Z80. Ни одна из ранее эмулированных мной машин на основе Z80 не использовала NMI, а потому особого смысла воссоздавать эту функцию не было — я всё равно не смог бы проверить её работу.

Если вы не знаете, что такое Bomb Jack, то эта игра выглядела так (не уверен, что подобрал соотношение сторон правильно):

Bomb Jack Screenshot

Версию эмулятора на WebAssembly можно изучить здесь:

https://floooh.github.io/tiny8bit/bombjack.html

После завершения процедуры загрузки и появления экрана таблицы рекордов нажмите 1, чтобы забросить монету, а затем Enter (или любую другую клавишу, кроме стрелок и пробела), чтобы запустить игру.

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

Исходный код находится здесь:

https://github.com/floooh/chips-test/blob/master/examples/sokol/bombjack.c

В нём используются заголовки чипов для обеспечения эмуляции Z80 и AY-3-8910, а также заголовки sokol в качестве кроссплатформенной обёртки (для входа в приложение, рендеринга, ввода и звука).

Шаг 1: исследования

«Исследования» — это слишком громкое слово: я просто вбил в Гугле «Bombjack arcade hardware specs».

По сравнению с популярными домашними компьютерами 80-х (или даже загадочными восточноевропейскими компьютерами, у которых часто находятся всё ещё активные сообщества), в Интернете очень мало информации о Bomb Jack.

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

Также существует проект, который реализует Bomb Jack на FGPA, из VHDL-исходников которого мне удалось узнать подробности, отсутствующие в принципиальной схеме.

Разобраться в исходниках MAME было бы сложновато, потому что эмуляторы аркадных автоматов — это обычно просто куча макросов, описывающих способ взаимодействия различных частей оборудования, но самого исходного кода не так много.

Тем не менее, макро-описания оборудования, и особенно комментарии, всё равно оказались очень полезны для понимания работы «железа», а там, где они становились слишком загадочными (например часть о декодировании видео), достаточно было метода проб и ошибок, а также подробного изучения принципиальной схемы.

Обзор оборудования

Самое интересное в «железе» Bomb Jack то, что на самом деле это два компьютера, соединённых друг с другом «изолентой»: есть основная плата с ЦП Z80 и оборудованием для декодирования видео и отдельная звуковая плата с собственным ЦП Z80 и тремя (да, тремя!) звуковыми чипами AY-3-8910.

Оборудование для декодирования видео не реализовано как интегральная схема — это просто множество небольших чипов общего назначения (их схема занимает 6 из 10 страниц принципиальной схемы устройства). При создании эмулятора я решил пойти короткой дорогой: вместо эмуляции отдельных частей оборудования декодирования видео я эмулировал только его поведение, создавая из входных данных соответствующие выходные и особо не беспокоясь о том, как работает само оборудование посередине.

Такое упрощённое решение вполне подходит для отдельного аркадного автомата, который предназначен для запуска только одной программы. Если игра запускается и работает правильно, то эмуляцию можно считать «достаточно хорошей».

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

Это также значит, что мои готовые эмуляторы ЦП и звукового чипа на самом деле являются лишней работой для Bomb Jack, например, работа ЦП Z80 с реализацией дробности машинного цикла — это перебор, достаточно было бы более простой и быстрой дробности на уровне инструкций.

Основная плата

Обычно первое, что я пытаюсь выяснить при написании нового эмулятора — это схема распределения памяти (где находятся области ПЗУ и ОЗУ, видеопамять и специальные адреса или порты ввода-вывода).

На основной плате Bomb Jack есть только один «интересный» чип — ЦП Z80, работающий на частоте 4 МГц. Всё оставшееся пространство основной платы занято оборудованием декодирования видео (за исключением пары чипов ОЗУ и ПЗУ).

16-битное адресное пространство выглядит следующим образом:

  • 0000..7FFF: 32 КБ ПЗУ
  • 8000..8FFF: 4 Кбайт ОЗУ общего назначения
  • 9000..93FF: 1 Кбайт видеопамяти
  • 9400..97FF: 1 Кбайт цветовой ОЗУ
  • 9820..987F: 96 байт спрайтовой ОЗУ
  • 9C00..9CFF: 256 байт ОЗУ цветовой палитры
  • 9E00, B000..B005, B800: порты ввода-вывода
  • C000..DFFF: 8 Кбайт ПЗУ

Область портов ввода-вывода выглядит следующим образом. Некоторые порты только для записи, некоторые только для чтения, а некоторые имеют разные функции при чтении и записи в них:

  • 9E00: запись: номер текущего фонового изображения, чтение: —
  • B000: чтение: состояние джойстика игрока 1, запись: включение/отключение маски NMI
  • B001: чтение: состояние джойстика игрока 2, запись: —
  • B002: чтение: монеты и кнопки Start, запись: —
  • B003: чтение: сторожевой таймер ЦП, запись: ???
  • B004: чтение: dip-переключатели 1, запись: переключение экрана
  • B005: чтение: dip-переключатели 2, запись: —
  • B800: запись: команда звуковой плате, чтение: —

Здесь стоит упомянуть следующее:

  • В устройстве МНОГО ПЗУ (40 Кбайт), и совсем мало ОЗУ (около 7 Кбайт, и только 4 Кбайт из них «ОЗУ общего назначения»)
  • Для «ОЗУ дисплея» выделено всего 2 Кбайта, разделённых на два фрагмента по 1 Кбайт, чего кажется очень мало для полноцветного дисплея 256x256, в котором, похоже, цвета задаются попиксельно
  • Это система с вводом-выводом в схеме распределения памяти!

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

Взгляд на принципиальную схему это подтверждает: у ЦП основной платы не обнаруживается контакта IORQ, подсоединён только контакт MREQ (который используется для инициализации чтения или записи в память):

Bomb Jack IORQ

Это значит, что нам не стоит волноваться о запросах ввода-вывода функции таймера ЦП основной платы в эмуляторе, а заниматься только запросами памяти.

Изучив принципиальную схему, я нашёл ещё одну интересную деталь о ЦП основной платы:

Соединён только контакт NMI, в то время как у контакта INT всегда поддерживается высокий уровень тактового сигнала/он остаётся неактивным (это значит, что «обычные» маскируемые прерывания не выполняются, и происходят только немаскируемые):

Bomb Jack IORQ

Это тоже довольно необычно для машины с Z80. Во всех домашних компьютерах на основе Z80, с которыми я раньше имел дело, всё было наоборот — они использовали только маскируемые прерывания, и никогда не применяли немаскируемые. Маскируемое прерывание Z80 — это очень гибкое и серьёзное улучшение по сравнению с примитивной системой прерываний его «внебрачного отца» — Intel 8080, или его конкурента — MOS 6502. Но эту повышенную гибкость одновременно и сложнее реализовывать в оборудовании (если только в качестве источника прерываний не используются другие чипы семейства Z80, в которых уже есть встроенный сложный протокол прерываний при подключении шиной).

Ну да ладно, хватит подробностей про оборудование, перейдём к эмулятору!

Процедура загрузки

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

Удивительно, но такого грубого подхода часто достаточно для прохождения процедуры загрузки и отображения на экране чего-нибудь. При разработке эмулятора Bomb Jack я просто взял содержимое видеопамяти размером 1 Кбайт в диапазоне от 0x9000 до 0x93FF как матрицу байт 32x32. Когда байт был равен 0, я рендерил блок чёрных пикселей 8x8, а в противном случае — блок белых пикселей.

Затем я просто запускал эмулируемый ЦП и надеялся на лучшее. Узрите! Появилась какая-то разборчивая картинка:

Bomb Jack Boot 1

Bomb Jack Boot 2

Верхнее изображение выглядит как экран теста оборудования при загрузке, а нижнее — как экран рекордов очков, которое появляется после завершения процедуры загрузки:

Bomb Jack Boot 3

Bomb Jack Boot 4

…но повёрнутая на 90 градусов (что логично, потому что экран аркадных автоматов часто находился в вертикальной «портретной» ориентации).

Отлично, начало многообещающее!

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

Поначалу всё шло довольно быстро, на тестовом экране во время загрузки отображались пиксели и цвета (позже я заметил, что декодирование цветов было совершенно неверным, и тем не менее...):

Bomb Jack Boot 5

Но когда должен был появиться экран рекордов, я получал чёрный экран. Хакнув фоновый цвет так, чтобы он был «не чёрным», я обнаружил, что пиксели рендерятся, но вся цветовая палитра чёрная. Хм…

Bomb Jack Boot 5

Посмотрев на этот экран пару минут, я вспомнил, что некоторые цвета на экране рекордов анимированы, а когда есть анимация, то должен быть и какой-то таймер. Логичным источником времени в этой конфигурации оборудования будет сигнал VSYNC дисплея, а VSYNC подключен к контакту NMI ЦП (или, скорее не VSYNC, а VBLANK, который является кратким моментом между сигналом VSYNC и перемещением луча электронно-лучевой трубки в левый верхний угол).

А всё это я пока не реализовал…

Следующим вечером, когда я добавил в эмуляцию Z80 первую версию обработки NMI и подключил её к первому попавшемуся счётчику vsync/vblank в функции таймера ЦП основной платы, внезапно начало происходить очень многое!

Во-первых, на экране рекордов появились цвета, и некоторые из них были анимированными:

Bomb Jack NMI 1

Через несколько секунд начиналось нечто ещё более восхитительное! Экран рекордов пропадал, и отображалась странная визуализация первой карты. Было понятно, что это демо-режим аркадного автомата для привлечения внимания — я видел несколько бомб с анимацией цвета, которые исчезали, когда воображаемый Bomb Jack прыгал по карте, собирая эти бомбы:

Bomb Jack NMI 2

Цвета по-прежнему были совершенно неверными, и тем не менее это ПРОГРЕСС!

Настало подходящее время заняться остальной частью декодирования видео:

Видеожелезо

На первый взгляд, оборудование работы с видео в Bomb Jack выглядело очень мощным для 8-битной машины из 1984 года: несмотря на разрешение всего 256x256 пикселей, оно могло одновременно отображать 128 (из 4096) цветов, и рендерить до 24 аппаратных спрайтов (размером 16x16 или 32x32) с попиксельным заданием цветов.

У 8-битных домашних компьютеров того времени было примерно такое же разрешение дисплея, но у них существовало множество ограничений относительно цветов. Эти ограничения очень чётко заметны при сравнении версий Bomb Jack для ZX Spectrum и Amstrad CPC с версией для аркадного автомата:

У версии для ZX Spectrum было довольно хорошее пиксельное разрешение (256x192), но очень мало цветов, и она страдала от типичного для Spectrum эффекта «конфликта цветов» (хоть разработчики и хорошо постарались для того, чтобы это было не слишком заметно):

Bomb Jack ZX

Версия для Amstrad CPC более полноцветная, но чтобы получить больше цветов, разработчикам пришлось перейти к режиму дисплея низкого разрешения (160x200). В результате этого Jack и монстры превратились в неразборчивую кучку пикселей:

Bomb Jack CPC

Сравните это с версией для аркадного автомата, которая имела то же разрешение в пикселях, что и у ZX Spectrum, но с гораздо большим количеством цветов и повышенным попиксельным разрешением цветов:

Bomb Jack Arcade

Интересно здесь то, что аркадная версия имеет более качественную графику не потому, что работает на более мощном железе (у неё больше ПЗУ для хранения большего объёма графических данных, но «вычислительная мощь» приблизительно такая же), а потому, что разработчики устройства могли сосредоточиться на изготовлении специализированной машины для одного конкретного типа игры и им не нужно было создавать универсальный домашний компьютер общего назначения.

Вот, как работает оборудование дисплея (по крайней мере, в моей высокоуровневой интерпретации):

Три слоя дисплея

Готовый видеосигнал Bomb Jack скомбинирован из трёх слоёв: фонового слоя, переднего слоя и слоя спрайтов.

Такая система слоёв имеет два основных преимущества:

  • Она реализует довольно хитрое аппаратное сжатие изображений для генерации полноцветного изображения «высокого разрешения» по очень малому объёму данных
  • Она значительно снижает объём работы ЦП, необходимой для обновления динамических элементов экрана (даже при частоте 4 МГц 8-битный ЦП не имеет достаточной мощности, чтобы перемещать такое количество объектов по дисплею 256x256 с частотой 60 Гц)

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

Фоновый слой

Фоновый слой может рендерить 1 из 5 фоновых изображений, залитых в ПЗУ. Фоновое изображение выбирается записью значения от 1 до 5 по адресу 0x9E00 (похоже, значение 0 является специальным и рендерит совершенно чёрный фон).

На самом деле, кажется, что оборудование способно рендерить 7 разных изображений, но в игре используется только 5. Втайне я надеялся найти в ПЗУ необнаруженные ранее данные изображений. Но увы, их там нет (да наверно я и не первый, кто их там искал).

Вот как выглядит фоновый слой первой карты без двух других слоёв:

Bomb Jack Background

Фоновый слой собран из тайлов 16x16 пикселей.

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

Bomb Jack Background Tiles

Для экономии памяти оборудование фонового слоя реализует ещё один трюк — тайлы можно переворачивать по горизонтали. Я почти упустил это в своей реализации, потому что предположил, что ПО не использует эту аппаратную функцию, но заметил в фоне третьей карты небольшой баг:

Bomb Jack Background 3

Тот же трюк использовал на пятой карте, но здесь его заметить немного сложнее, если не знать, что искать:

Bomb Jack Background 5

Передний слой:

Поверх фонового слоя находится «передний слой», который рендерит все неподвижные части экрана, которые тем не менее должны обновляться ЦП (в основном это текст, платформы и бомбы). Схема расположения считывается из ОЗУ (из фрагментов 1-кбайтного ОЗУ и 1-кбайтного цветового ОЗУ).

Вот как выглядит изолированный передний слой первой карты:

Bomb Jack Foreground

Передний слой тоже состоит из тайлов (как и фоновый), но в нём используются меньшие тайлы размером 8x8:

Bomb Jack Background Tiles

Основное преимущество разделения фона и передней части на отдельные слои заключается в том, что ЦП не нужно беспокоиться о хранении и восстановлении пикселей фона при создании или удалении передних элементов.

Слой спрайтов

И наконец поверх переднего слоя рендерятся аппаратные спрайты. Всё, что перемещается по экрану, реализовано в спрайтах. Оборудование Bomb Jack может рендерить до 24 спрайтов, а каждый спрайт может иметь размер 16x16 или 32x32 пикселя. При этом спрайты можно располагать с попиксельной точностью:

Bomb Jack Sprites

Декодер тайлов 8x8

В «сердце» оборудования декодирования видео находится цветовая палитра с 128 элементами и декодером тайлов размером 8x8 пикселей. Задача декодера тайлов заключается в генерации 7-битного индекса цветовой палитры для каждого из 64 пикселей тайла.

Эти тайлы размером 8x8 являются строительными блоками для всего на экране — фоновых тайлов 16x16, тайлов переднего слоя 8x8 и аппаратных спрайтов 16x16 или 32x32.

Вот блок-схема этого декодера тайлов 8x8 для рендеринга переднего слоя (так, как я его понял):

Bomb Jack Tile Decoding

Объяснение блок-схемы сверху вниз:

  • Процесс декодирования начинается в верхней части со считывания байта «кода тайла» из видеопамяти (организованной как матрица 32x32 кодов тайлов) и отдельного байта из цветового ОЗУ (тоже матрица 32x32). Получение кодов тайлов и цветов из видеопамяти происходит только для переднего слоя, но я добавил его, чтобы была понятнее картина в целом. Самому декодеру тайлов 8x8 требуются на входе только код тайлов и цветов.
  • Код тайлов используется как индекс для поиска по трём отдельным пиксельным битовым слоям. Эти пиксельные битовые слои всегда хранятся в ПЗУ (пиксельные битовые слои можно воспринимать как данные шрифтов или спрайтшиты). Каждый из трёх слоёв дисплея имеет собственные ПЗУ тайлов, и эти ПЗУ видимы только декодирующему оборудованию, но не ЦП (чтобы они не занимали драгоценное адресное пространство ЦП).
  • Каждый пиксельный битовый слой состоит из 8 байтов на тайл, и каждый байт содержит 8 пикселей (один бит на пиксель). Так как данные пикселей для каждого тайла собираются из трёх битовых слоёв, это означает, что для описания внешнего вида тайла 8x8 требуется 24 байт данных ПЗУ (3 бита на пиксель).
  • Для каждого из 64 пикселей тайла создаётся 7-битное значение. Нижние 3 бита берутся из битовых слоёв тайлов из ПЗУ тайлов, а верхние 4 бита — из байта значения цвета. По сути, это означает, что каждый тайл может выбирать в цветовой палитре один из 16 «слотов», где каждый слот содержит 8 цветов. Каждый пиксель тайла может выбирать один из 8 цветов в слоте палитры тайла.
  • Этот 7-битовый индекс, собираемый из битовых слоёв и значения цвета тайла, используется для поиска 12-битного значения RGB-цвета из цветовой палитры (4 бита на цветовой канал). Цветовая палитра расположена в ОЗУ и с ней может работать ЦП (насколько я видел, ОЗУ видео, цветов и палитр используются только для записи; по крайней мере, ЦП никогда не выполняет доступ для чтения в эти области).

Это общая схема декодирования тайлов, которая используется каждым из трёх слоёв дисплея, но декодирование каждого слоя немного отличается:

  • Передний слой на самом деле может рендерить 512 разных тайлов 8x8. Для этого требуются 9-битные коды тайлов, но видеопамять предоставляет только 8 бит на тайл. Девятый бит «заимствуется» из пятого бита значения цвета (так как для построения индекса цветовой палитры используется всего 4 бита значения цвета, остаётся ещё 4 бита для других целей). Если все 3 бита из битовых слоёв тайла 8x8 равны нулю, то передний пиксель считается прозрачным, а фоновый пиксель «просвечивает» сквозь него.
  • В фоновом слое используются тайлы 16x16, поэтому ему необходимо всего 16x16=256 значений кодов тайлов и 256 значений цвета для описания фонового изображения в ПЗУ фоновых изображений (512 байт на изображение). Хитрость здесь в том, что битовые слои пикселей 16x16 выстроены в виде четырёх тайлов 8x8, поэтому можно использовать то же самое оборудование декодирования. Как сказано выше, фоновые тайлы можно отзеркаливать по горизонтали; эта операция контролируется одним из «запасных» битов значений цвета: если бит 7 значения цвета задан, то тайл будет отзеркален.
  • Каждый аппаратный спрайт может иметь размер 16x16 пикселей или 32x32 пикселя, а битовые слои тоже состоят из 4 или 16 последовательных тайлов 8x8 в ПЗУ спрайтовых тайлов. Это значит, что для спрайта 16x16 требуется 96 байтов, а для спрайта 32x32 — чудовищный объём в 384 байт в ПЗУ тайлов. Как и в случае с передними тайлами, если все 3 бита битового слоя пикселей равны нулю, то пиксель спрайта прозрачен.

Чтобы лучше понять, как выглядят битовые слои тайлов, я написал небольшую программу на C, преобразующую ПЗУ тайлов в файлы PNG (3 бита на пиксель преобразованы в 8 уровней оттенков серого).

Ниже показан ПЗУ тайлов переднего слоя. Мы видим данные чисел и текстового шрифта, тайлы платформ, бомбы (разделённые пополам), части логотипа из заставки Bomb Jack, и числа множителей очков, которые появляются в верхней части экрана (кстати, всё повёрнуто на 90 градусов, потому что весь экран тоже повёрнут):

Bomb Jack Foreground Tile ROM

Далее рассмотрим ПЗУ тайлов фона. Оно выглядит не особо понятно, потому что наблюдаемое нами на самом деле является декодированием тайлов 16x16 в тайлы 8x8. Каждый тайл 16x16 создан из четырёх соседних тайлов 8x8. Но можно распознать части греческого храма с карты 2, замка с карты 3 и небоскрёбов с карты 4.

Bomb Jack Background Tile ROM

И, наконец, ПЗУ спрайтовых тайлов. Спрайты 16x16 занимают верхнюю половину, а спрайты 32x32 — нижнюю.

Bomb Jack Sprite Tile ROM

Интересный хак экрана заставки Bomb Jack заключается в том, что логотип собран из передних тайлов и спрайтов. Думаю, что у разработчиков заканчивалась ПЗУ передних тайлов, но оставалось немного места в ПЗУ спрайтов:

Bomb Jack Splash 1

Bomb Jack Splash 2

Bomb Jack Splash 3

Спрайтовое оборудование

Спрайтовое оборудование Bomb Jack очень мощное по сравнению с тем, которое использовалось в домашних компьютерах того времени:

  • Оно могло рендерить до 24 аппаратных спрайтов. Похоже, что ограничений на количество спрайтов на строку развёртки не было.
  • Спрайты могли иметь размер 16x16 пикселей или 32x32 пикселя
  • Каждый спрайт мог выбирать один из 16 слотов по 8 цветов в общей цветовой палитре
  • Спрайты имели попиксельное цветовое разрешение.
  • Каждый спрайт можно было переворачивать по вертикали или горизонтали
  • Каждый спрайт мог выбирать один из 128 прошитых в ПЗУ спрайтовых изображений.

При декодировании пикселей и спрайтов спрайтовой системы используется тот же базовый тайл 8x8, что и в фоновом и переднем слоях.

Атрибуты спрайтов размещены в диапазоне адресов от 0x9820 до 0x987F — 96 байт, по 4 байта на спрайт. Насколько я увидел, эта область только для чтения; по крайней мере, ЦП не выполняет доступ для чтения в этот диапазон памяти.

Каждый спрайт описывается 4 байтами:

  • Байт 0:
    • Бит 7: если задан, то это спрайт размером 32x32, в противном случае размером 16x16
    • Биты 6..0: 7 бит на задание кода тайла спрайта, используемого для поиска битовых слоёв спрайтового изображения в ПЗУ тайлов.
  • Байт 1:
    • Бит 7: если задан, то спрайт отзеркален по горизонтали
    • Бит 6: если задан, спрайт отзеркален по вертикали
    • Биты 3..0: 4 бит для задания значения цвета для декодера тайлов

  • Байт 2: позиция спрайта на экране по оси X
  • Byte 3: позиция спрайта на экране по оси Y

Непонятно, что делают биты 4 и 5 байта 1, в комментарии в MAME говорится об этом:

e ? (задаётся, когда выбираются большие спрайты)
f ? (задаётся, только когда материализуется бонус (B)?)

Размещённые в памяти порты ввода-вывода

Несколько замечаний по портам ввода-вывода основной платы. Как сказано выше, порты ввода-вывода выглядят так:

  • 9E00: запись: номер текущего фонового изображения, чтение: —
  • B000: чтение: состояние джойстика игрока 1, запись: включение/отключение маски NMI
  • B001: чтение: состояние джойстика игрока 2, запись: —
  • B002: чтение: монеты и кнопки Start, запись: —
  • B003: чтение: сторожевой таймер ЦП, запись: ???
  • B004: чтение: dip-переключатели 1, запись: переключение экрана
  • B005: чтение: dip-переключатели 2, запись: —
  • B800: запись: команда звуковой плате, чтение: —

Адрес 0x9E00 (выбор фонового изображения) мы уже рассматривали выше, а адрес 0xB800 (команда звуковой плате) мы рассмотрим в следующем разделе. Остаются адреса с 0xB000 по 0xB005:

Считывание из адресов 0xB000 и 0xB001 возвращает текущее состояние двух джойстиков. Заданные байты обозначают замкнутые переключатели джойстика:

  • бит 0: направление «вправо»
  • бит 1: направление «влево»
  • бит 2: направление «вверх»
  • бит 3: направление «вниз»
  • бит 4: нажата кнопка «прыжок»

Оставшиеся 3 бита игнорируются.

Считывание из 0xB002 возвращает состояние монетоприёмника и кнопок Start:

  • бит 0: вброшена монета игрока 1
  • бит 1: вброшена монета игрока 2
  • бит 2: кнопка Start игрока 1
  • бит 3: кнопка Start игрока 2

Считывание из адресов 0xB004 и 0xB005 возвращает состояние dip-переключателей, которые используются для настройки поведения аркадного автомата:

  • B004:
    • биты 0,1: сколько «игр» даётся за одну монету (1, 2, 3 или 5)
    • биты 2,3: то же самое для игрока 2
    • биты 4,5: сколько жизней на игру (3, 4, 5 или 2)
    • бит 6: расположение аркадного автомата: «столик для коктейлей» или «вертикальный».
    • бит 7: нужно ли воспроизводить звук в режиме ожидания
  • B005:
    • биты 3,4: сложность 1 (скорость птицы)
    • биты 5,6: сложность 2 (количество и скорость врагов)
    • бит 7: частота появления особой монеты

И, наконец, считывание из адреса B003 реализует программный сторожевой таймер. ЦП должен часто выполнять считывание из этого адреса, в противном случае аркадный автомат выполнит аппаратный сброс. Если по какой-то причине в игре возникнет сбой, то оборудование автоматически перезагрузится.

В некоторые адреса портов ввода-вывода можно выполнять запись:

  • B000: нужно ли генерировать NMI во время vblank; похоже, отключено только во время процедуры загрузки
  • B004: переворот всего экрана; ни разу не встречал использования этой функции, но у меня есть теория на её счёт (см. ниже)

Функционал переворота экрана немного сбивает с толку, потому что играя в игру, я никогда не видел его применения. Однако у меня есть догадка о том, что он делает, но чтобы подтвердить её, нужно написать код. Когда аркадный автомат находится в конфигурации «столик для коктейлей», два игрока сидят друг напротив друга. Поэтому я предположил, что когда игра переключается с игрока 1 на игрока 2, эта функция переворачивает экран. Однако я пока не реализовал в эмуляторе режим двух игроков.

Звуковая плата

Звуковая плата сама по себе является полнофункциональным компьютером с ЦП Z80 (работающим на частоте 3 МГц), тремя звуковыми чипами (AY-38910, работающими на частоте 1,5 МГц), а также с ОЗУ и ПЗУ. Схема распределения памяти звуковой платы выглядит довольно просто:

  • 0000..2000: 8 Кбайт ПЗУ
  • 4000..4400: 1 Кбайт ОЗУ
  • 6000: звуковая команда от основной платы

Так как выше адреса 0x8000 в схеме распределения памяти нет ничего интересного, самый верхний адресный контакт ЦП даже не присоединён:

Bomb Jack Sound A15

Особый адрес 0x6000 — это размещённый в памяти порт ввода-вывода (8-битная защёлка), не соответствующая настоящей ОЗУ. Это тот же порт, который размещён на основной плате по адресу 0xB800. Он является каналом связи между основной и звуковой платами.

Три звуковых чипа управляются настоящими инструкциями вывода Z80, а не через размещённые в памяти порты. У AY-3-8910 открыто только два порта ввода-вывода, первый используется для хранения номера регистра, а второй — для записи или чтения содержимого регистра, заданного первым портом.

Схема ввода-вывода выглядит следующим образом:

  • 0x00: первый звуковой чип: выбор регистра
  • 0x01: первый звуковой чип: доступ к выбранному регистру
  • 0x10: второй звуковой чип: выбор регистра
  • 0x11: второй звуковой чип: доступ к выбранному регистру
  • 0x80: третий звуковой чип: выбор регистра
  • 0x81: третий звуковой чип: доступ к выбранному регистру

Пара слов о звуковом чипе AY-3-8910:

Это довольно стандартное устройство, очень популярное и в домашних компьютерах того времени (например в Amstrad CPC, ZX Spectrum 128, в компьютерах MSX и многих других). AY-3-8910 породил множество вариаций и клонов (например, Yamaha YM2149, который сам по себе стал основой целого семейства более мощных звуковых чипов).

AY-3-8910 имеет 3 канала прямоугольных сигналов, один генератор шума, который можно смешивать с тремя каналами, и один генератор огибающей. Так как для всех трёх каналов существовал всего один генератор огибающей, он был не особо полезен, и большинство игр для модуляции тона и громкости использовало ЦП.

Это значит, что чипу AY-3-8910 для создания качественного звука требуется большее вмешательство ЦП (в отличие от более автономных чипов SID, например, в компьютере C64).

Удивительно видеть, что можно сделать на трёх достаточно простых звуковых чипах и управляющем ими ЦП. Музыка и звуковые эффекты Bomb Jack гораздо насыщенней, чем мне приходилось слышать в большинстве игр для домашних компьютеров.

Единственное, что действительно интересно в этой звуковой плате — способ получения ею своих команд от основной платы.

Защёлка звуковой команды

«Звуковая защёлка» — это однобайтное хранилище (8-битная защёлка), общая для основной и звуковой плат. Защёлка привязана к адресу 0xB800 на основной плате и к адресу 0x6000 на звуковой плате.

При включении с помощью VSYNC прерывания NMI звуковая плата выполняет очень простую процедуру обслуживания прерываний, которая считывает аппаратную защёлку, записывает её по адресу обычной памяти и задаёт «сигнальный бит», сообщающий «основному циклу», что получена новая звуковая команда:

        ex af,af'               ;0066
        exx                     ;0067
        ld hl,04390h            ;0068
        set 0,(hl)              ;006b
        ld a,(06000h)           ;006d
        ld (04391h),a           ;0070
        exx                     ;0073
        ex af,af'               ;0074
        retn                    ;0075

Способ активации контакта NMI немного отличается от способа для основной платы:

На основной плате контакт NMI становится активным на время выполнения VBLANK.

Однако на звуковой плате NMI активируется при срабатывании VSYNC, и остаётся активным не в течение VBLANK, а до того момента, пока процедура обслуживания прерываний не считает данные из защёлки по адресу 0x6000.

Когда оборудование распознаёт считывание с адреса 0x6000, оно выполняет две жёстко заданные операции:

  • содержимое звуковой защёлки сбрасывается на 0
  • контакт NMI становится неактивным

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

Остался единственный вопрос: как часто основная плата записывает новую команду (потому что от этого зависит способ реализации эмуляции двух плат).

Выполнив отладку с помощью printf, я обнаружил, что основная плата записывает не более одной звуковой команды на один 60-герцовый кадр. Это сильно упростило структуру «основного цикла» эмулятора.

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

Например, наихудший случай был бы таким:

  • выполняем в компьютере 1 одну инструкцию
  • выполняем в компьютере 2 одну инструкцию
  • повторяем…

Мой эмулятор Z80 не оптимизирован для выхода и входа в эмуляцию для каждой инструкции, потому что в этом случае он должен сбрасывать в память и загружать из памяти состояние ЦП в начале и конце каждой инструкции. Если ЦП может без помех обрабатывать множество инструкций, то можно хранить (бОльшую часть) состояния ЦП в регистрах и сбрасывать состояние в память на последней инструкции.

То есть идеальная ситуация была бы такой: выполняем без помех эмулируемую систему в течение всего кадра хост-системы (для ЦП с частотой 4 МГц и при 60 Гц это означает около 67 тысяч циклов на кадр, или где-то от 3 тысяч до 16 тысяч инструкций Z80).

При работе с Bomb Jack мне нужно было убедиться, что основная плата не записывает новую команду до того, как звуковая плата окажется способной считать последнюю команду. Прежде чем я выяснил, что основная плата записывает не больше одной команды на кадр, я рассматривал необходимость создания сложной очереди команд, которая бы перехватывала записи в звуковую защёлку основной платы и сохраняла в очередь номер цикла и байт команды.

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

Такая система работала бы и была бы «правильной», но сильно повысила бы сложность кода.

В конце концов я решил использовать гораздо более простое решение без всяких очередей. Так как основная плата записывает только по одной команде на кадр, я чередовал выполнение на двух компьютерах так, чтобы каждый из них выполнял по два временных среза на кадр:

  • выполняем на основной плате первую половину кадра
  • выполняем на звуковой плате первую половину кадра
  • выполняем на основной плате вторую половину кадра
  • выполняем на звуковой плате вторую половину кадра

Это гарантирует, что звуковая плата правильно увидит каждую команду, записанную основной платой, и в то же время сможет выполнять каждую эмуляцию в течение тысяч циклов.

Разумеется, то, что хост-система работает приблизительно с частотой кадров 60 Гц — это очень смелое предположение :)

И последнее…

Последний интересный факт о версии эмулятора на WebAssembly:

Сжатый размер всех скачиваемых файлов при выполнении эмулятора на WebAssembly
примерно равен 113 Кбайтам:

  • около 2,5 Кбайта на HTML, CSS и на «рукописный» JS
  • 26,8 Кбайт на файл emscripten runtime JS
  • 83,7 Кбайт на файл .wasm

Файл WASM содержит встроенные ПЗУ аркадного автомата.

В несжатом виде эти ПЗУ занимают 112 Кбайта.

То есть весь сжатый эмулятор со встроенными ПЗУ занимает почти тот же объём, что и несжатые ПЗУ :)

112-килобайтные ПЗУ сжимаются примерно до 57 Кбайт, то есть истинный размер сжатого кода в WASM без данных ПЗУ занимает меньше 30 Кбайт (84 — 57).

Мне кажется, совсем неплохо для полного эмулятора 8-битной системы ;)

Автор: PatientZero

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js