Здравствуйте!
В предыдущей части данного цикла статей мы рассмотрели процессор и память DMG. Следующий логичный шаг — эмуляция того, как DMG выводит изображение.
Пишем эмулятор Gameboy, часть 1
Пишем эмулятор Gameboy, часть 2
Пишем эмулятор Gameboy, часть 3
Таймеры
Управление
Сводим все воедино
Тестирование
Заключение
DMG выводит изображение на экране построчно и эмулирует состояния, характерные для ЭЛТ экранов. Каждое состояние длится строго определенное число тактов. Необходимо это для обеспечения доступа к памяти. Для отрисовки графики доступ к видеопамяти и OAM необходим сразу в двух местах – LCD-контроллеру, который выводит все на экран; игре (CPU), которая модифицирует память, чтобы вывести тот кадр, который ей нужен. Для решения этой проблемы вся задача вывода графики была поделена на интервалы (каждому соответствует определенное состояние), которые и определяют время, в течение которого логика экрана или игра может получить доступ к памяти. Всего состояний четыре (номера не случайные, а строго определенные для DMG):
При отрисовке каждой строки LCD-контроллер проходит через состояния в таком порядке – 2, 3, 0. После отрисовки последней строки он переходит в состояние 1. Затем все начинается заново с первой строки.
Отрисовка одной строки длится ровно 456 тактов. Это время складывается из длительности состояний 2, 3, 0 и всегда равно 456 тактам, но длительность самих состояний может варьироваться. Поскольку экран имеет 144 строки, то их вывод занимает 65664 тактов. Еще ровно 4560 тактов длится состояние 1. Из цифры видно, что это время равно 10 строкам. Это действительно так – в состоянии 1 происходит как бы отрисовка еще 10 строк. Счетчик строк (регистр LY) не останавливается на 143, а доходит до 153. В итоге, полное обновление экрана занимает 70224 тактов или 154 строки по 456 тактов.
Переход между состояниями сопровождается запросами прерываний, если они разрешены. Переход в каждое из четырех состояний, кроме третьего, сопровождается запросом прерывания LCDC. Помимо этого, это прерывание запрашивается, если LY и LYC равны. Запрос осуществляется только если LCDC прерывание для данного состояния разрешено в регистре STAT. Его структура выглядит следующим образом:
Важная деталь – LCDC прерывание может быть запрошено только один раз за строку.
Много из этого можно было бы почерпнуть из CPU Manual, но тут есть одно но – это далеко не все, что нужно знать о работе LCD-контроллера для его эмуляции. Я не вдавался во все подробности (собственно, я их и не нашел), а лишь остановился на том, что позволяет корректно выводить графику в протестированных мной играх, а заодно проходить тест LCD-контроллера.
Чтобы хотя бы примерно эмулировать контроллер, нам придется ввести еще один набор состояний – внутренний, доступный лишь для нашего эмулятора. Всего их 8:
Из названий видно, что они соответствуют реальным состояниям. Проходят они в следующем порядке:
Промежуточные состояния нужны для более точной синхронизации различных событий. Рассмотрим каждое состояние подробнее.
LCDMODE_LYXX_OAM. При переходе в это состояние мы изменяем в регистре STAT состояние на 2 (чтение OAM). Проверяем, разрешено ли LCDC прерывание. В случае успеха запрашиваем его и помечаем где-то, что LCDC прерывание больше запрашивать нельзя.
В этом состоянии мы учитываем одну особенность DMG. Если у регистра SCX установлен бит 2 (например, регистр равен 4), то именно сейчас нам надо где-то отметить, что следующие состояния должны изменить свою длительность на 4 такта. Данное состояние длится ровно 80 тактов.
LCDMODE_LYXX_OAMRAM. Изменяем в STAT состояние на 3. LCDC прерывания нет. Пока мы не касались темы спрайтов, но именно здесь будет находиться вызов функции, которая составляет очередь спрайтов, которые будут позже выведены на данной строке.
Это еще одна особенность DMG. Вывод спрайтов изменяет длительность состояний. Чем больше их, тем дольше длится состояние 3 и тем короче состояние 0 (нам надо уложиться в 456 тактов, поэтому все пропорционально). DMG может вывести максимум 10 спрайтов в одной строке, поэтому нам хватит массива из 11 элементов со значениями, которые указывают изменение длительности состояний. В нем должны быть следующие значения:
Данное состояние длится 172 такта + кол-во тактов из-за регистра SCX + кол-во тактов из-за спрайтов. Например, если регистр SCX на предыдущем состоянии был равен 4, а спрайтов выводится 6, то длительность состояния будет равна 172 + 4 + 64 такта.
LCDMODE_LYXX_HBLANK. Здесь мы отрисовываем текущую строку, которую можно узнать из регистра LY. Отмечаем в регистре STAT состояние 0. Запрашиваем LCDC прерывание, если оно еще не было запрошено.
Длина такта высчитывается так: 200 тактов – такты из-за SCX – такты из-за спрайтов. Таким образом, здесь мы компенсируем то смещение в длительности, которое произошло в состоянии LCDMODE_LYXX_OAMRAM.
LCDMODE_LYXX_HBLANK_INC. Здесь мы увеличиваем регистр LY на единицу — он является счетчиком строк. Обнуляем флаг, который указывает, что прерывание LCDC уже было запрошено (мы ведь переходим на следующую строку). Здесь же нам необходимо проверить на равенство регистры LY и LYC. Вот примерный псевдокод:
Регистр LYC используется играми для отслеживания момента, когда LY достигнет определенного значения. Других назначений у него нет.
Данное состояние длится 4 такта. В какое состояние будет осуществляться переход, зависит от значения LY. Все указано в таблице выше.
LY9X_VBLANK. Если мы перешли в это состояние и LY равен 144, то нам надо как-то отменить, что мы перешли в состояние V-blank. Устанавливаем в STAT состояние 1. Запрашиваем прерывание V-blank. Здесь же надо опять запросить LCDC прерывание, если оно разрешено. Таким образом, здесь может быть запрошено два прерывания. Если LY не равен 144, то ничего этого делать не надо, т.к. мы и так в V-blank.
Данное состояние длится ровно 452 такта.
LY9X_VBLANK_INC. Здесь нам надо инкрементировать LY и проверить на равенство LY и LYC, как мы это делали раньше. Здесь же надо учесть, что при LY равном 153 мы переходим в другое состояние, а не начинаем заново с LY9X_VBLANK – см. таблицу.
Длится данное состояние 4 такта.
LY00_VBLANK. Здесь нам надо обнулить LY.
Длится данное состояние 452 такта.
LCDMODE_LY00_HBLANK. Это последнее в кадре и довольно странное состояние. Оно длится всего 4 такта и устанавливает в регистре STAT состояние 0. После него все начинается заново.
Все, с циклом состояний покончено. Теперь небольшая, но очень важная деталь. Если вы прочитали описание регистра LCDC, то могли заметить, что бит 7 отвечает за вкл/выкл дисплея. Нам надо как-то отразить это в наших состояниях.
Если игра выключила дисплей, то нам надо обнулить регистр LY, а в регистре STAT установить состояние 0. Если игра включила дисплей (обязательное условие – до этого он должен был быть выключен), то нам надо вернуть контроллер в самое начальное состояние – все счетчики тактов обнулены, LY равен нулю, текущим является состояние 2. Т.е. мы начинаем весь цикл состояний с самого начала. Многие игры отказываются работать без этих манипуляций. Примером такой игры является Bomb Jack. Она отказывается доходить даже до начального меню.
Я не случайно привожу примеры игр. Тестовые ROM'ы это хорошо, но их прохождение не гарантирует корректную работу игр, да и не все они проверяют. Где возможно, я буду приводить название игр, которые рекомендуется проверить и на своем эмуляторе. Bomb Jack проверять обязательно — такие трюки с выключением экрана проделывают многие игры.
Во всех компонентах синхронизация будет выглядеть примерное одинаково. Заводим счетчик тактов, к которому будем прибавлять прошедшие такты. Как только значение достигло необходимого, мы вычитаем из него это число (не обнуляем, ведь могло пройти больше тактов, чем нам нужно) и делаем то, что нужно.
Вот как будет выглядеть наша функция синхронизации в Cookieboy::GPU:
Естественно это лишь часть, но ее достаточно. И так, ClockCounter служит счетчиком. Далее у нас следует конструкция while, которая проверяет, достиг ли счетчик нужного значения. Переменная ClocksToNextState служит для хранения числа тактов, которое должно пройти до наступления нового состояния. Их мы и вычитаем из счетчика, чтобы он продолжил отсчитывать уже до следующего состояния.
Зачем здесь while? Это важно. Некоторые состояния длятся всего 4 такта и вполне возможно, что это состояние может наступить сразу же. Т.е. установили мы ClocksToNextState, равной 4, а в нашем счетчике и так 4 такта. Чтобы нам не ждать лишний вызов функции синхронизации, мы обработаем новое событие тут же, на следующей итерации цикла. Данный подход стоит взять за правило там, где интервалы между состояниями (событиями) слишком малы и могут быть меньше длительности одной инструкции процессора.
Далее мы видим условие с макросом LCD_ON(). Здесь мы проверяем, не отключен ли дисплей. Если это так, то мы не проходим через весь цикл состояний. Мы лишь очищаем экран и ждем положенные для одного полного обновления экрана 70224 такта.
Если же дисплей включен, то далее с помощью конструкции switch осуществляем необходимые действия для текущего состояния. Специфика моей реализации такова, что мы храним в LCDMode не столько текущее, сколько следующее состояние. Выполнив необходимые действия, мы устанавливаем в LCDMode следующее состояние и показываем, сколько тактов до него должно пройти. Да, LCDMode лишь вспомогательная переменная, для игр она не существует. Реальные состояния хранятся в регистре STAT, где и должны.
Вывод графики осуществляется в три этапа в следующем порядке:
![](https://habrastorage.org/r/w1560/storage2/319/c03/3fb/319c033fb2fe1674d23fa72e81289af7.png)
Как видно, если мы вышли за границы фона, то мы оказываемся на его противоположном конце. Фон затайлен, как часто говорят.
Вся информация о тайлах и их содержании находится в видеопамяти. Вот ее структура:
Вначале хранятся сами тайлы. DMG может хранить до 384 тайлов, поделенных на два набора по 256 тайлов так, что половина из них у наборов общая. Один набор использует для обозначения тайлов номера от 0 до 255. Другой – номера от -128 до 127. Сам фон рисуется согласно выбранной карте тайлов. Их тоже две. Они имеют размер 1024 байта – по одному байту на номер тайла.
Выбор набора тайлов и карты тайлов осуществляется с помощью регистра LCDC. Там же есть флаг, который вкл/выкл отображение фона. Вот, кстати, его структура:
Сами тайлы занимают в памяти по 16 байт. Каждые 2 байта отвечают за одну строку, таким образом, давая нам тайлы размером 8х8. Сама организация тайлов в памяти довольно странная, как показано ниже:
![](https://habrastorage.org/r/w1560/storage2/a99/53c/8fe/a9953c8fecb3451f3b591b407addd184.png)
Цвет пикселя составляется из двух бит, где младший бит берется из первого байта, а старший из второго. В результате получаются индексы цветов, которые могут иметь 4 значения: от 0 до 3. Эти индексы используются для выбора цвета из палитры в регистре BGP. Вот его структура:
Т.е., имея цвет пикселя равный 2, мы смотрим в регистре BGP значение битов 5-4, которые и дают нам цвет, который тоже может иметь 4 значения от 0 до 3. Это приводит нас к необходимости иметь еще одну палитру для перевода цветов из палитры DMG в настоящие RGB цвета для последующего вывода. Относится это ко всей графике в целом.
Можно использовать черно-белую палитру, что дает нам следующие цвета:
Или же использовать цвета, более приближенные к таковым на экране настоящего DMG:
Цвет со значением 0 (самый яркий) используется для очистки экрана. Соответственно, если экран надо очистить или фон отключен, то просто заливаем все цветом с индексом 0.
На рисунке ниже показан пример вывода «окна» при WX=87 и WY=70.
![](https://habrastorage.org/r/w1560/storage2/af0/aa7/5d7/af0aa75d710059103d9cc67a61d94a76.png)
Перед выводом надо проверить не только флаг вывода «окна» в LCDC, но и координаты:
Важная деталь. WX и WY могут быть изменены в процессе вывода. Изменения WX вступят в силу уже при выводе следующей строки, а вот изменения WY вступят в силу только на следующем обновлении экрана.
Как я уже говорил, во многом вывод «окна» идентичен выводу фона, но есть одно серьезное отличие.
Для его вывода используется скрытый указатель текущей строки «окна», который инкрементируется после вывода очередной строки. Если «окно» отключено или скрыто из-за координат WX/WY, то его счетчик строк не инкрементируется. Таким образом, если вывод «окна» был отключен на полпути, то при включении вывода в дальнейшем вывод продолжится с того места, на котором он остановился. Справедливо это в течение одного обновления экрана. В конце V-blank счетчик строк «окна» обнуляется.
Помимо этого, значение счетчика изменяется при модификации LCDC. Если «окно» было выключено, а сейчас включено посредством LCDC, то его вывод начнется только на следующем обновлении экрана с первой строки.
Как минимум одна игра использует данную особенность DMG – Ant Soldiers. Сразу после запуска внизу экрана должны отображаться авторы игры. Если при выводе «окна» не учесть упомянутые особенности, то внизу экрана будет пусто. Но еще хуже то, что игровой интерфейс так же не будет виден, из-за чего нормальная игра уже невозможна.
Спрайты могут иметь размер 8х8 или 8х16, т.е. один или два тайла, что контролируется флагом в регистре LCDC. Информация о спрайтах находится в области памяти OAM. На один спрайт приходится 4 байта, что позволяет хранить до 40 спрайтов в OAM. Эти байты содержат следующую информацию:
Координаты спрайта указаны для нижнего правого угла. Координаты верхнего левого угла равны X-8 и Y-16. Размер спрайта здесь значения не имеет.
Для номера тайла очень важно учитывать размер спрайта. Если он установлен как 8x16, то необходимо обнулить самый младший бит в номере тайла, иначе вывод графики в некоторых играх будет некорректен.
Если приоритет установлен в 1, то спрайт рисуется как бы за фоном и «окном». Пиксели спрайта рисуются только поверх цветов, которые имеют значение равное нулю. Для этого надо вывести фон и «окно», а затем проверить цвет пикселя, где собираемся выводить спрайт. Спрайт как бы «просвечивается» через пиксели с нулевым цветом. Если же приоритет установлен в 0, то спрайт рисуется поверх фона и «окна».
С отражением все понятно. Палитр для спрайтов две (OBP0 и OBP1), выполняют они туже роль с тем исключением, что индекс цвета 0 (биты 0-1 в палитре) означает прозрачный пиксель и его цвет в палитре не имеет значения.
Перед тем как вывести спрайты, необходимо узнать, какие именно нужно выводить и в каком порядке. У спрайтов существует приоритет, который диктует порядок их вывода на экран. Вычисляется он так – спрайты выводятся в порядке их координат X, от больших к меньшим. Т.е. спрайты с меньшим значением координаты X выводятся поверх тех, что с большим значением X. Если координаты X равны, то приоритет вычисляется согласно порядку следования в OAM – спрайты с меньшим адресом в OAM будут выше.
Чтобы определить, нужно ли выводить спрайт, необходимо проверить координату Y из OAM. Она должна быть таковой, чтобы спрайт попадал на текущую линию:
![](https://habrastorage.org/r/w1560/storage2/566/c09/515/566c09515ef41febee5dec6c7e17307f.png)
где LY это текущая строка экрана, а SpriteHeight – высота спрайта (8 или 16). Это очень важная формула – неправильное формирование очереди спрайтов приведет к трудноуловимым багам в некоторых играх (многие игры могут отлично работать, как это было у меня). Сначала мы переносим координату Y – как уже было сказано, координаты спрайтов указывают на их нижний правый угол. Опять же, высота спрайта на конкретно эти вычисления не влияет, а вот для формирования интервала, в котором может лежать значение текущей строки, нам уже надо учитывать высоту спрайта.
Координата X может быть любой, даже если она приведет к тому, что спрайт не будет виден на экране – он все равно попадает в список.
Выводятся спрайты согласно их приоритету, но не более 10 штук на одной строке. Таким образом, невидимые из-за координаты X спрайты попадают в список и тем самым ограничивают возможное число спрайтов на данной строке экрана. Соответственно, при выводе спрайтов на экран нужно быть готовым к тому, что они могут лежать за его пределами.
Что касается ограничения в 10 штук – я так и не нашел достоверной информации о том, как надо учитывать это ограничение. Тут может быть два логичных варианта решения:
Я выбрал второй вариант, поскольку другие эмуляторы используют именно его. Тестирование в играх не помогло с решением – видимых багов какой-то из реализаций не наблюдалось.
Больше всего проблем мне, наверное, доставила игра Gameboy Wars Turbo. Добиться правильной картинки в ней я смог только к моменту написания данного цикла статей. Есть и другие хорошие тестовые игры. Ant Soldiers я уже упоминал. Для тестирования вывода спрайтов хорошо подходит Kirbys Pinball Land — вступительных ролик использует спрайты различного размера.
Как обычно, объявляем все необходимые нам регистры, даем к ним доступ извне (конкретно, для класса Cookieboy::Memory). С видеопамятью и OAM чуть сложнее.
Как помните, ранее было сказано, что эта память доступна не всегда и зависит это от текущего состояния в регистре STAT. Появляется вопрос – реализовывать как надо (блокировать доступ к памяти в нужный момент) или как проще (память доступна всегда). Тут все зависит от того, насколько точна эмуляция переходов из состояния в состояние. Несоблюдение длительности интервалов может привести к тому, что память будет недоступна для чтения и записи тогда, когда должна быть доступна. В моей реализации оказалось возможным блокировать доступ – протестированные мной игры отлично работают в таких условиях. На ранних этапах моего проекта эмуляция LCD-контроллера была скудной (все делал как написано в Gameboy CPU Manual), и блокирование доступа приводило к выводу мусора на экране. Не все эмуляторы это делают, но решать вам.
Так же нам необходим буфер кадра, а можно даже два, как я и сделал. Один буфер кадра состоит из индексов цветов – этот буфер необходим, поскольку те же спрайты требуют проверки цвета под ними для правильного вывода. Второй буфер содержит копию того, что находится в первом буфере, но цвета уже представлены в RGB значениях для дальнейшего вывода на экран.
Для вывода графики нам необходимо завести две функции. Одну для вывода текущей строки (LY) на экран, складывающуюся из фона, «окна» и спрайтов — void Cookieboy::GPU::RenderScanline(). Другую для формирования очереди спрайтов для последующего вывода — void Cookieboy::GPU::PrepareSpriteQueue(). В разделе про логику LCD-контроллера я уже упоминал, куда нужно поместить вызовы этих функций.
Собственно, говорить о реализации этих функций нечего – либо говорить все от и до (так, конечно же, неинтересно), либо ничего не говорить. Тут у вас есть свобода действий – ваша задача лишь получить на экране правильную картинку. Какими путями вы к этому придете уже не важно. По исходному коду различных эмуляторов это заметно – подходы везде совершенно разные, в то время как другие компоненты имеют чуть ли не идентичную реализацию.
Подведем итоги. На данный момент наш эмулятор может исполнять код и даже выводить изображение на экран. Это все хорошо, но мы все еще не можем запускать тестовые ROM’ы для оценки проделанной работы. Большинство тестов подсчитывают время, а для этого нам надо реализовать еще один компонент DMG – таймеры.
Первый счетчик, который мы будем называть DIV, работает на частоте 16384 Гц (т.е. отсчет происходит каждые 256 тактов процессора). Как принцип, так и его реализация предельно просты. С указанной частотой происходит инкрементирование значения в регистре DIV. DIV зациклен от 0 до 255. Т.е. если его значение равно 255, то следующий отсчет установит в нем значение 0. Если попытаться записать в DIV что-то самостоятельно, то его значение будет обнулено.
Второй счетчик, TIMA, немного сложнее и имеет больше возможностей. В общей сложности под него отведено три регистра:
Реализация практически такая же, как у DIV, только с учетом упомянутых особенностей. Здесь же стоит вспомнить об использовании while (кстати, здесь можно использовать более быстрый и не менее точный подход без цикла) как это было сделано с LCD-контроллером. При частоте 262144 Гц период составляет всего 16 тактов, а значит может возникнуть ситуация, когда за один раз нам придется сделать несколько отсчетов. Если счетчик будет равен двум и более периодам, то простое условие приведет к накоплению ошибки, из-за чего таймер будет работать неверно. На начальных этапах у меня были подобные проблемы.
И так, на данный момент у нас все готово для тестирования нашего эмулятора с помощью тестовых ROM’ов. Перед этим все же коснемся еще одной темы – управления.
![](https://habrastorage.org/r/w780q1/storage2/037/321/ced/037321cedd6d335de9a8fb502320fc5c.jpg)
Как можно видеть на фотографии, DMG имеет 8 кнопок: 4-направленная крестовина, кнопки A и B, кнопки Start и Select. Чтобы игра могла узнать, какие кнопки нажаты в данный момент, DMG записывает пользовательский ввод в регистр P1. Вот его структура:
Смотря на таблицу, мы можем заметить, что битов на все кнопки здесь не хватает. В действительности, состояния кнопок записываются только в P13, P12, P11, P10. Биты P15 и P14 модифицируются ROM’ом игры для того, чтобы выбрать, состояние какой четверки клавиш мы хотим считывать через регистр P1. Вот таблица соответствия:
В этом регистре бит со значением 0 означает, что клавиша нажата. Если клавиша нажата, то запрашивается соответствующее прерывание. Именно это прерывание выводит DMG из состояния останова вследствие исполнения инструкции STOP. В таком состоянии основные компоненты не работают, и прерывания не запрашиваются. Только нажатие на кнопку может запросить его. В моем эмуляторе инструкция STOP игнорируется — игры работают нормально, а тестовые ROM'ы ее не проверяют.
Реализация сводится к опрашиванию состояния клавиш, которые эмулируют кнопки DMG, и установке соответствующих битов согласно значениям в P15 и P14. Но есть и свои нюансы.
Опрашивать физические клавиши (клавиатура, геймпад, не важно) каждый раз, скорее всего, не получится – производительность будет хуже некуда. Собственно, этого и не требуется. Лучше завести счетчик тактов и проверять состояние клавиш только по достижении им определенного значения. Какое? Решайте сами, я использовал длительность полного обновления экрана – 70224 такта. Этого достаточно для комфортной игры. Пока счетчик отмеряет время до следующего опроса клавиш, мы устанавливаем биты регистра P1 согласно полученным ранее состояниям клавиш.
В первую очередь нам нужна индикация того, что эмулятор закончил обновление экрана или отработал 70224 такта. Далее заводим цикл обновления, на каждый итерации которого наш эмулятор должен завершить одно обновление экрана. Параллельно с этим мы должны замерить время, которое уйдет у ПК на эмуляцию этого обновления экрана. Скорее всего, оно будет довольно маленьким и нам придется «усыпыть» приложение на некоторые время, чтобы в итоге одна итерация цикла заняла 1000/60 мс. Не забываем только вовремя опрашивать клавиатуру.
Именно здесь можно реализовать популярную в эмуляторах ускоренную эмуляцию, когда мы позволяем эмулятору работать на максимально возможной скорости.
И так, что мы имеем. На данный момент наш эмулятор содержит все функции, необходимые для запуска игр. Вся остальная функциональность является необязательной, но будет рассмотрена позже. Сейчас нам важно проверить корректность эмуляции с помощью тестовых ROM’ов.
Вот ссылка на оригиналы тестов и еще одна на всякий случай, мало ли что случится с сайтом автора, тесты очень и очень полезные. Помимо самих тестов по ссылкам можно найти их исходный код. Крайне рекомендую разобраться в ассемблере — понимание того, как и что тестируется, очень сильно поможет в исправлении ошибок. Сейчас это может быть не так важно, но в тестах звука без понимания исходного кода исправить ошибку иной раз будет попросту невозможно.
Тест процессора проверяет результат и флаги после исполнения каждой инструкции. В случае ошибки на экране будет выведен опкод неправильной инструкции. Данный пакет тестов состоит из 11 тестов различных групп инструкций. Именно отдельные тесты покажут, какие конкретно инструкций неправильно реализованы. Скорее всего, на данном этапе у вас будет достаточно много ошибок, в основном связанных с флагами. Больше всего проблем мне лично доставили POP AF, DAA и HALT. Правильную реализацию POP AF я уже упомянул – нет смысла наступать на те же грабли еще раз и вам, ничего кроме потери времени и интереса это не принесет. Мое негодование насчет настолько тривиальной инструкции не имело пределов, а всего-то надо было обнулить неиспользуемые биты в регистре F. DAA сложна сама по себе и вполне возможно будет проще и надежнее взять чью-то реализацию. HALT я так же упомянул во всех подробностях. Слепое следование CPU Manual привело к еще одной череде негодований…
Естественно не стоит полностью полагаться на тест – сам тестовый ROM может работать неправильно из-за неправильно реализованных инструкций или их длительности. Стоит сразу озаботиться о как можно более точной реализации всех аспектов DMG, чтобы с помощью тестов уже выявить только то, что вы не заметили или авторы забыли указать в документации.
Особенно это важно для тестов, связанных с тактами. Тест длительности инструкций процессора проверяет длительность каждой инструкции в тактах, в том числе, учитывая разницу в длительности у инструкций условных переходов. В Readme этого теста есть таблица с тактами.
Вполне возможно, что тест при запуске выдаст ошибку 255. Этой ошибки нет в исходном коде теста, отчего я вернулся к уже привычному для меня состоянию негодования, но после более тщательного анализа его кода стала понятна причина – тестовый ROM перед запуском проводит некоторые проверки и устанавливает таймеры для дальнейших измерений. Именно эти внутренние проверки не проходят. Для этого пришлось вооружиться таблицами из Readme, после чего тест успешно заработал и показал, где у меня еще закрались ошибки. В инструкциях условных переходов и было дело, практически все остальное было верно.
Тест синхронизации памяти и процессора проверяет состояние памяти в течение процесса исполнения инструкции процессора. Если вы вызываете функции синхронизации только после исполнения инструкции, то тест будет провален. Я уже упоминал о том, как сделать правильно, поэтому пройти этот тест уже не составляет особого труда. Если что, все необходимые данные тест выведет на экране. Если интересно как он работает, то все элементарно. Все операции производятся на счетчике таймера, установленного на максимальной частоте. При правильной синхронизации таймера с исполнением тестовой инструкции получится, что в процессе (именно в процессе) исполнения счетчик должен увеличиться. После исполнения эти значения и проверяются.
Наконец, тест LCD. Он проверяет, насколько точно соблюдается длительность состояний LCD-контроллера, и когда происходят различные события (например, увеличение счетчика строк LY). Без прохождения этого теста не стоит даже и задумываться о том, чтобы эмулировать блокировку видеопамяти и OAM в различных состояниях. Так же он проверит корректность вкл/выкл дисплея посредством 7 бита в регистре LCDC. Я уже упоминал, что манипуляции ROM’а с этим битом требуют определенных действий с нашей стороны.
Играть можно, но без звука эмулятор все же выглядит неполноценным. Именно о звуке и пойдет речь в следующей статье. Тема эта большая, относительно сложная с кучей подводных камней, но очень и очень интересная.
В предыдущей части данного цикла статей мы рассмотрели процессор и память DMG. Следующий логичный шаг — эмуляция того, как DMG выводит изображение.
Пишем эмулятор Gameboy, часть 1
Пишем эмулятор Gameboy, часть 2
Пишем эмулятор Gameboy, часть 3
Оглавление
ДисплейТаймеры
Управление
Сводим все воедино
Тестирование
Заключение
Дисплей
На данном этапе нам необходимо эмулировать то, как DMG отображает на экране картинку. Все будет находиться в классе Cookieboy::GPU (ссылка на исходный код Cookieboy). Задачу можно поделить на две большие части – эмуляция специфики того, как DMG рисует картинку; эмуляция логики, которая управляет экраном.LCD-контроллер. Теория
Начнем с логики, поскольку именно она будет диктовать, когда и что нам рисовать. Как всегда, перед реализацией нам надо понять, как работает компонент.DMG выводит изображение на экране построчно и эмулирует состояния, характерные для ЭЛТ экранов. Каждое состояние длится строго определенное число тактов. Необходимо это для обеспечения доступа к памяти. Для отрисовки графики доступ к видеопамяти и OAM необходим сразу в двух местах – LCD-контроллеру, который выводит все на экран; игре (CPU), которая модифицирует память, чтобы вывести тот кадр, который ей нужен. Для решения этой проблемы вся задача вывода графики была поделена на интервалы (каждому соответствует определенное состояние), которые и определяют время, в течение которого логика экрана или игра может получить доступ к памяти. Всего состояний четыре (номера не случайные, а строго определенные для DMG):
- 0. H-blank. Для ЭЛТ-экранов означает, что в этот момент сканирующий луч переходит в начало следующей строки. У DMG, естественно, никаких лучей нет. Это состояние означает две вещи. Первое, была выведена одна строка. Второе, видеопамять и OAM не используются LCD-контроллером и доступны CPU.
- 1. V-blank. Еще одно состояние из мира ЭЛТ, которое означает момент, когда луч дошел до конца последней строки и переходит в начало первой строки. Для нас оно означает две вещи. Первое, были выведены все 144 видимые строки. Второе, видеопамять и OAM не используются LCD-контроллером и доступны CPU.
- 2. OAM. Данное состояние означает, что LCD-контроллер использует OAM память. CPU она не доступна, зато пока еще доступна видеопамять.
- 3. OAMRAM. Данное состояние означает, что LCD-контроллер использует OAM и видеопамять. CPU они недоступны.
При отрисовке каждой строки LCD-контроллер проходит через состояния в таком порядке – 2, 3, 0. После отрисовки последней строки он переходит в состояние 1. Затем все начинается заново с первой строки.
Отрисовка одной строки длится ровно 456 тактов. Это время складывается из длительности состояний 2, 3, 0 и всегда равно 456 тактам, но длительность самих состояний может варьироваться. Поскольку экран имеет 144 строки, то их вывод занимает 65664 тактов. Еще ровно 4560 тактов длится состояние 1. Из цифры видно, что это время равно 10 строкам. Это действительно так – в состоянии 1 происходит как бы отрисовка еще 10 строк. Счетчик строк (регистр LY) не останавливается на 143, а доходит до 153. В итоге, полное обновление экрана занимает 70224 тактов или 154 строки по 456 тактов.
Переход между состояниями сопровождается запросами прерываний, если они разрешены. Переход в каждое из четырех состояний, кроме третьего, сопровождается запросом прерывания LCDC. Помимо этого, это прерывание запрашивается, если LY и LYC равны. Запрос осуществляется только если LCDC прерывание для данного состояния разрешено в регистре STAT. Его структура выглядит следующим образом:
Биты | Назначение |
6 | Разрешить LCDC прерывание в случае равенства регистров LY и LYC |
5 | Разрешить LCDC прерывание при переходе в состояние 2 |
4 | Разрешить LCDC прерывание при переходе в состояние 1 |
3 | Разрешить LCDC прерывание при переходе в состояние 0 |
2 | Бит установлен, если LY и LYC равны. Сброшен иначе |
0-1 | Текущее состояние |
Важная деталь – LCDC прерывание может быть запрошено только один раз за строку.
Много из этого можно было бы почерпнуть из CPU Manual, но тут есть одно но – это далеко не все, что нужно знать о работе LCD-контроллера для его эмуляции. Я не вдавался во все подробности (собственно, я их и не нашел), а лишь остановился на том, что позволяет корректно выводить графику в протестированных мной играх, а заодно проходить тест LCD-контроллера.
Чтобы хотя бы примерно эмулировать контроллер, нам придется ввести еще один набор состояний – внутренний, доступный лишь для нашего эмулятора. Всего их 8:
enum InternalLCDModes
{
LCDMODE_LY00_HBLANK,
LCDMODE_LYXX_HBLANK,
LCDMODE_LYXX_HBLANK_INC,
LCDMODE_LY00_VBLANK,
LCDMODE_LY9X_VBLANK,
LCDMODE_LY9X_VBLANK_INC,
LCDMODE_LYXX_OAM,
LCDMODE_LYXX_OAMRAM
};
Из названий видно, что они соответствуют реальным состояниям. Проходят они в следующем порядке:
Строка | Состояния |
0 | LYXX_OAM -> LYXX_OAMRAM -> LYXX_HBLANK -> LYXX_HBLANK_INC |
1 | LYXX_OAM -> LYXX_OAMRAM -> LYXX_HBLANK -> LYXX_HBLANK_INC |
... | ... |
143 | LYXX_OAM -> LYXX_OAMRAM -> LYXX_HBLANK -> LYXX_HBLANK_INC |
144 | LY9X_VBLANK -> LY9X_VBLANK_INC |
... | ... |
152 | LY9X_VBLANK -> LY9X_VBLANK_INC |
153 | LY00_VBLANK -> LY00_HBLANK |
Промежуточные состояния нужны для более точной синхронизации различных событий. Рассмотрим каждое состояние подробнее.
LCDMODE_LYXX_OAM. При переходе в это состояние мы изменяем в регистре STAT состояние на 2 (чтение OAM). Проверяем, разрешено ли LCDC прерывание. В случае успеха запрашиваем его и помечаем где-то, что LCDC прерывание больше запрашивать нельзя.
В этом состоянии мы учитываем одну особенность DMG. Если у регистра SCX установлен бит 2 (например, регистр равен 4), то именно сейчас нам надо где-то отметить, что следующие состояния должны изменить свою длительность на 4 такта. Данное состояние длится ровно 80 тактов.
LCDMODE_LYXX_OAMRAM. Изменяем в STAT состояние на 3. LCDC прерывания нет. Пока мы не касались темы спрайтов, но именно здесь будет находиться вызов функции, которая составляет очередь спрайтов, которые будут позже выведены на данной строке.
Это еще одна особенность DMG. Вывод спрайтов изменяет длительность состояний. Чем больше их, тем дольше длится состояние 3 и тем короче состояние 0 (нам надо уложиться в 456 тактов, поэтому все пропорционально). DMG может вывести максимум 10 спрайтов в одной строке, поэтому нам хватит массива из 11 элементов со значениями, которые указывают изменение длительности состояний. В нем должны быть следующие значения:
Кол-во спрайтов | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
Такты | 0 | 8 | 20 | 32 | 44 | 52 | 64 | 76 | 88 | 96 | 108 |
Данное состояние длится 172 такта + кол-во тактов из-за регистра SCX + кол-во тактов из-за спрайтов. Например, если регистр SCX на предыдущем состоянии был равен 4, а спрайтов выводится 6, то длительность состояния будет равна 172 + 4 + 64 такта.
LCDMODE_LYXX_HBLANK. Здесь мы отрисовываем текущую строку, которую можно узнать из регистра LY. Отмечаем в регистре STAT состояние 0. Запрашиваем LCDC прерывание, если оно еще не было запрошено.
Длина такта высчитывается так: 200 тактов – такты из-за SCX – такты из-за спрайтов. Таким образом, здесь мы компенсируем то смещение в длительности, которое произошло в состоянии LCDMODE_LYXX_OAMRAM.
LCDMODE_LYXX_HBLANK_INC. Здесь мы увеличиваем регистр LY на единицу — он является счетчиком строк. Обнуляем флаг, который указывает, что прерывание LCDC уже было запрошено (мы ведь переходим на следующую строку). Здесь же нам необходимо проверить на равенство регистры LY и LYC. Вот примерный псевдокод:
Если LY == LYC
Если бит 2 в регистре STAT сброшен
Устанавливаем бит 2 в регистре STAT
Запрашиваем LCDC прерывание, если оно разрешено
Иначе
Сбрасываем бит 2 в регистре STAT
Регистр LYC используется играми для отслеживания момента, когда LY достигнет определенного значения. Других назначений у него нет.
Данное состояние длится 4 такта. В какое состояние будет осуществляться переход, зависит от значения LY. Все указано в таблице выше.
LY9X_VBLANK. Если мы перешли в это состояние и LY равен 144, то нам надо как-то отменить, что мы перешли в состояние V-blank. Устанавливаем в STAT состояние 1. Запрашиваем прерывание V-blank. Здесь же надо опять запросить LCDC прерывание, если оно разрешено. Таким образом, здесь может быть запрошено два прерывания. Если LY не равен 144, то ничего этого делать не надо, т.к. мы и так в V-blank.
Данное состояние длится ровно 452 такта.
LY9X_VBLANK_INC. Здесь нам надо инкрементировать LY и проверить на равенство LY и LYC, как мы это делали раньше. Здесь же надо учесть, что при LY равном 153 мы переходим в другое состояние, а не начинаем заново с LY9X_VBLANK – см. таблицу.
Длится данное состояние 4 такта.
LY00_VBLANK. Здесь нам надо обнулить LY.
Длится данное состояние 452 такта.
LCDMODE_LY00_HBLANK. Это последнее в кадре и довольно странное состояние. Оно длится всего 4 такта и устанавливает в регистре STAT состояние 0. После него все начинается заново.
Все, с циклом состояний покончено. Теперь небольшая, но очень важная деталь. Если вы прочитали описание регистра LCDC, то могли заметить, что бит 7 отвечает за вкл/выкл дисплея. Нам надо как-то отразить это в наших состояниях.
Если игра выключила дисплей, то нам надо обнулить регистр LY, а в регистре STAT установить состояние 0. Если игра включила дисплей (обязательное условие – до этого он должен был быть выключен), то нам надо вернуть контроллер в самое начальное состояние – все счетчики тактов обнулены, LY равен нулю, текущим является состояние 2. Т.е. мы начинаем весь цикл состояний с самого начала. Многие игры отказываются работать без этих манипуляций. Примером такой игры является Bomb Jack. Она отказывается доходить даже до начального меню.
Я не случайно привожу примеры игр. Тестовые ROM'ы это хорошо, но их прохождение не гарантирует корректную работу игр, да и не все они проверяют. Где возможно, я буду приводить название игр, которые рекомендуется проверить и на своем эмуляторе. Bomb Jack проверять обязательно — такие трюки с выключением экрана проделывают многие игры.
LCD-контроллер. Реализация
Наконец мы можем приступить к реализации рассмотренной теории. Как я уже говорил, в нашем эмуляторе мы должны синхронизировать с процессором все остальные компоненты – LCD-контроллер не исключение. Для этого мы заведем функцию в классе Cookieboy::GPU, которая на вход принимает число прошедших тактов. Здесь мы и будет осуществлять все, что связано со сменой состояний.Во всех компонентах синхронизация будет выглядеть примерное одинаково. Заводим счетчик тактов, к которому будем прибавлять прошедшие такты. Как только значение достигло необходимого, мы вычитаем из него это число (не обнуляем, ведь могло пройти больше тактов, чем нам нужно) и делаем то, что нужно.
Вот как будет выглядеть наша функция синхронизации в Cookieboy::GPU:
void Cookieboy::GPU::Step(DWORD clockDelta, Interrupts &INT)
{
ClockCounter += clockDelta;
while (ClockCounter >= ClocksToNextState)
{
ClockCounter -= ClocksToNextState;
if (!LCD_ON())
{
LY = 0;
//очистка экрана
ClocksToNextState = 70224;
continue;
}
switch (LCDMode)
{
case LCDMODE_LY00_VBLANK:
LY = 0;
LCDMode = LCDMODE_LY00_HBLANK;
ClocksToNextState = 452;
break;
}
}
}
Естественно это лишь часть, но ее достаточно. И так, ClockCounter служит счетчиком. Далее у нас следует конструкция while, которая проверяет, достиг ли счетчик нужного значения. Переменная ClocksToNextState служит для хранения числа тактов, которое должно пройти до наступления нового состояния. Их мы и вычитаем из счетчика, чтобы он продолжил отсчитывать уже до следующего состояния.
Зачем здесь while? Это важно. Некоторые состояния длятся всего 4 такта и вполне возможно, что это состояние может наступить сразу же. Т.е. установили мы ClocksToNextState, равной 4, а в нашем счетчике и так 4 такта. Чтобы нам не ждать лишний вызов функции синхронизации, мы обработаем новое событие тут же, на следующей итерации цикла. Данный подход стоит взять за правило там, где интервалы между состояниями (событиями) слишком малы и могут быть меньше длительности одной инструкции процессора.
Далее мы видим условие с макросом LCD_ON(). Здесь мы проверяем, не отключен ли дисплей. Если это так, то мы не проходим через весь цикл состояний. Мы лишь очищаем экран и ждем положенные для одного полного обновления экрана 70224 такта.
Если же дисплей включен, то далее с помощью конструкции switch осуществляем необходимые действия для текущего состояния. Специфика моей реализации такова, что мы храним в LCDMode не столько текущее, сколько следующее состояние. Выполнив необходимые действия, мы устанавливаем в LCDMode следующее состояние и показываем, сколько тактов до него должно пройти. Да, LCDMode лишь вспомогательная переменная, для игр она не существует. Реальные состояния хранятся в регистре STAT, где и должны.
Графика. Теория
DMG оперирует не пикселями, а тайлами. Естественно графика выводится попиксельно, но единицей для программиста служит именно тайл, размер которого составляет 8х8 пикселей. Таким образом, в памяти хранятся не цвета пикселей, а номера тайлов. Эти номера ссылаются на другую область памяти, где находится информация о тайлах – из пикселей какого цвета они состоят. Очевидная цель такого подхода – экономия памяти. Похоже на реализацию индексированных цветов.Вывод графики осуществляется в три этапа в следующем порядке:
- Фон (background)
- Так называемое «окно» (window)
- Спрайты (sprites)
Background
Фон у DMG имеет размер 32х32 тайла или 256х256 пикселей, что, очевидно, больше экрана DMG. С помощью регистров SCX и SCY мы можем указать, какую часть необходимо вывести на экран, как показано на рисунке ниже.![](https://habrastorage.org/storage2/319/c03/3fb/319c033fb2fe1674d23fa72e81289af7.png)
Как видно, если мы вышли за границы фона, то мы оказываемся на его противоположном конце. Фон затайлен, как часто говорят.
Вся информация о тайлах и их содержании находится в видеопамяти. Вот ее структура:
Секция | Назначение |
0x8000-0x87FF | Набор тайлов №1: тайлы [0, 127] |
0x8800-0x8FFF | Набор тайлов №1: тайлы [128, 255] Набор тайлов №0: тайлы [-128, -1] |
0x9000-0x97FF | Набор тайлов №0: тайлы [0, 127] |
0x9800-0x9BFF | Карта тайлов №0 |
0x9C00-0x9FFF | Карта тайлов №1 |
Вначале хранятся сами тайлы. DMG может хранить до 384 тайлов, поделенных на два набора по 256 тайлов так, что половина из них у наборов общая. Один набор использует для обозначения тайлов номера от 0 до 255. Другой – номера от -128 до 127. Сам фон рисуется согласно выбранной карте тайлов. Их тоже две. Они имеют размер 1024 байта – по одному байту на номер тайла.
Выбор набора тайлов и карты тайлов осуществляется с помощью регистра LCDC. Там же есть флаг, который вкл/выкл отображение фона. Вот, кстати, его структура:
Биты | Назначение |
7 | Управление LCD-контроллером: 0: выкл (экран пуст) 1: вкл |
6 | Выбор карты тайлов для «окна»: 0: Карта тайлов №0 (0x9800-0x9BFF) 1: Карта тайлов №1 (0x9C00-0x9FFF) |
5 | Флаг отображения «окна»: 0: выкл 1: вкл |
4 | Выбор набора тайлов для фона и «окна»: 0: Набор тайлов №0 (0x8800-0x97FF) 1: Набор тайлов №1 (0x8000-0x8FFF) |
3 | Выбор карты тайлов для фона: 0: Карта тайлов №0 (0x9800-0x9BFF) 1: Карта тайлов №1 (0x9C00-0x9FFF) |
2 | Размер спрайтов: 0: 8х8 1: 8:16 |
1 | Флаг отображения спрайтов: 0: выкл 1: вкл |
0 | Флаг отображения фона: 0: выкл 1: вкл |
Сами тайлы занимают в памяти по 16 байт. Каждые 2 байта отвечают за одну строку, таким образом, давая нам тайлы размером 8х8. Сама организация тайлов в памяти довольно странная, как показано ниже:
![](https://habrastorage.org/storage2/a99/53c/8fe/a9953c8fecb3451f3b591b407addd184.png)
Цвет пикселя составляется из двух бит, где младший бит берется из первого байта, а старший из второго. В результате получаются индексы цветов, которые могут иметь 4 значения: от 0 до 3. Эти индексы используются для выбора цвета из палитры в регистре BGP. Вот его структура:
Биты | Индекс цвета |
7-6 | 3 |
5-4 | 2 |
3-2 | 1 |
1-0 | 0 |
Т.е., имея цвет пикселя равный 2, мы смотрим в регистре BGP значение битов 5-4, которые и дают нам цвет, который тоже может иметь 4 значения от 0 до 3. Это приводит нас к необходимости иметь еще одну палитру для перевода цветов из палитры DMG в настоящие RGB цвета для последующего вывода. Относится это ко всей графике в целом.
Можно использовать черно-белую палитру, что дает нам следующие цвета:
Цвет в палитре | Значение RGB каналов |
0 | 0xFF, 0xFF, 0xFF |
1 | 0xAA, 0xAA, 0xAA |
2 | 0x55, 0x55, 0x55 |
3 | 0x00, 0x00, 0x00 |
Или же использовать цвета, более приближенные к таковым на экране настоящего DMG:
Цвет в палитре | Значение RGB каналов |
0 | 0xE1, 0xF7, 0xD1 |
1 | 0x87, 0xC3, 0x72 |
2 | 0x33, 0x70, 0x53 |
3 | 0x09, 0x20, 0x21 |
Цвет со значением 0 (самый яркий) используется для очистки экрана. Соответственно, если экран надо очистить или фон отключен, то просто заливаем все цветом с индексом 0.
Window
После вывода фона необходимо вывести «окно». Оно выводится практически так же как и фон – из LCDC узнаем, какую карту и набор тайлов использовать. Там же есть флаг вкл/выкл вывода. Выводится оно согласно координатам, указанным в регистрах WY и WX. Но чтобы вывести «окно» в левом верхнем углу экрана необходимо указать координаты WX = 7 и WY = 0. Т.е. координаты X и Y левого верхнего угла «окна» равны WX-7 и WY соответственно.На рисунке ниже показан пример вывода «окна» при WX=87 и WY=70.
![](https://habrastorage.org/storage2/af0/aa7/5d7/af0aa75d710059103d9cc67a61d94a76.png)
Перед выводом надо проверить не только флаг вывода «окна» в LCDC, но и координаты:
- если WX больше 166, то «окно» скрыто за пределами экрана;
- если WY больше 143, то «окно» так же скрыто.
Важная деталь. WX и WY могут быть изменены в процессе вывода. Изменения WX вступят в силу уже при выводе следующей строки, а вот изменения WY вступят в силу только на следующем обновлении экрана.
Как я уже говорил, во многом вывод «окна» идентичен выводу фона, но есть одно серьезное отличие.
Для его вывода используется скрытый указатель текущей строки «окна», который инкрементируется после вывода очередной строки. Если «окно» отключено или скрыто из-за координат WX/WY, то его счетчик строк не инкрементируется. Таким образом, если вывод «окна» был отключен на полпути, то при включении вывода в дальнейшем вывод продолжится с того места, на котором он остановился. Справедливо это в течение одного обновления экрана. В конце V-blank счетчик строк «окна» обнуляется.
Помимо этого, значение счетчика изменяется при модификации LCDC. Если «окно» было выключено, а сейчас включено посредством LCDC, то его вывод начнется только на следующем обновлении экрана с первой строки.
Как минимум одна игра использует данную особенность DMG – Ant Soldiers. Сразу после запуска внизу экрана должны отображаться авторы игры. Если при выводе «окна» не учесть упомянутые особенности, то внизу экрана будет пусто. Но еще хуже то, что игровой интерфейс так же не будет виден, из-за чего нормальная игра уже невозможна.
Sprites
Теперь настал черед спрайтов. Они так же состоят из тайлов, но выводятся совсем по-другому.Спрайты могут иметь размер 8х8 или 8х16, т.е. один или два тайла, что контролируется флагом в регистре LCDC. Информация о спрайтах находится в области памяти OAM. На один спрайт приходится 4 байта, что позволяет хранить до 40 спрайтов в OAM. Эти байты содержат следующую информацию:
Байт | Назначение |
0 | Y координата |
1 | X координата |
2 | Номер тайла (0-255) |
3 | Бит 7: приоритет Бит 6: зеркальное отражение по вертикали, если 1 Бит 5: зеркальное отражение по горизонтали, если 1 Бит 4: если 1, используем палитру OBJ1, иначе – OBJ0 |
Координаты спрайта указаны для нижнего правого угла. Координаты верхнего левого угла равны X-8 и Y-16. Размер спрайта здесь значения не имеет.
Для номера тайла очень важно учитывать размер спрайта. Если он установлен как 8x16, то необходимо обнулить самый младший бит в номере тайла, иначе вывод графики в некоторых играх будет некорректен.
Если приоритет установлен в 1, то спрайт рисуется как бы за фоном и «окном». Пиксели спрайта рисуются только поверх цветов, которые имеют значение равное нулю. Для этого надо вывести фон и «окно», а затем проверить цвет пикселя, где собираемся выводить спрайт. Спрайт как бы «просвечивается» через пиксели с нулевым цветом. Если же приоритет установлен в 0, то спрайт рисуется поверх фона и «окна».
С отражением все понятно. Палитр для спрайтов две (OBP0 и OBP1), выполняют они туже роль с тем исключением, что индекс цвета 0 (биты 0-1 в палитре) означает прозрачный пиксель и его цвет в палитре не имеет значения.
Перед тем как вывести спрайты, необходимо узнать, какие именно нужно выводить и в каком порядке. У спрайтов существует приоритет, который диктует порядок их вывода на экран. Вычисляется он так – спрайты выводятся в порядке их координат X, от больших к меньшим. Т.е. спрайты с меньшим значением координаты X выводятся поверх тех, что с большим значением X. Если координаты X равны, то приоритет вычисляется согласно порядку следования в OAM – спрайты с меньшим адресом в OAM будут выше.
Чтобы определить, нужно ли выводить спрайт, необходимо проверить координату Y из OAM. Она должна быть таковой, чтобы спрайт попадал на текущую линию:
![](https://habrastorage.org/storage2/566/c09/515/566c09515ef41febee5dec6c7e17307f.png)
где LY это текущая строка экрана, а SpriteHeight – высота спрайта (8 или 16). Это очень важная формула – неправильное формирование очереди спрайтов приведет к трудноуловимым багам в некоторых играх (многие игры могут отлично работать, как это было у меня). Сначала мы переносим координату Y – как уже было сказано, координаты спрайтов указывают на их нижний правый угол. Опять же, высота спрайта на конкретно эти вычисления не влияет, а вот для формирования интервала, в котором может лежать значение текущей строки, нам уже надо учитывать высоту спрайта.
Координата X может быть любой, даже если она приведет к тому, что спрайт не будет виден на экране – он все равно попадает в список.
Выводятся спрайты согласно их приоритету, но не более 10 штук на одной строке. Таким образом, невидимые из-за координаты X спрайты попадают в список и тем самым ограничивают возможное число спрайтов на данной строке экрана. Соответственно, при выводе спрайтов на экран нужно быть готовым к тому, что они могут лежать за его пределами.
Что касается ограничения в 10 штук – я так и не нашел достоверной информации о том, как надо учитывать это ограничение. Тут может быть два логичных варианта решения:
- Формирование очереди (проход по содержимому OAM) из всех видимых согласно координате Y спрайтов – сейчас их может быть больше 10. Далее сортировка и только после этого отбрасывание лишних спрайтов. Логично выкидывать спрайты с меньшим приоритетом.
- Формирование очереди прекращается, если набрано 10 спрайтов. Таким образом, может оказаться, что далее в OAM есть видимые спрайты с большим приоритетом, но мы до них не дошли.
Я выбрал второй вариант, поскольку другие эмуляторы используют именно его. Тестирование в играх не помогло с решением – видимых багов какой-то из реализаций не наблюдалось.
Графика. Реализация
Вот мы и подошли к еще одному ответственному этапу создания эмулятора. Малейшие ошибки здесь могут привести к ужасным последствиям на экране. Некоторые ошибки я нашел только в процессе написания данных статей. До этого приходилось наблюдать полный хаос на экране в некоторых играх.Больше всего проблем мне, наверное, доставила игра Gameboy Wars Turbo. Добиться правильной картинки в ней я смог только к моменту написания данного цикла статей. Есть и другие хорошие тестовые игры. Ant Soldiers я уже упоминал. Для тестирования вывода спрайтов хорошо подходит Kirbys Pinball Land — вступительных ролик использует спрайты различного размера.
Как обычно, объявляем все необходимые нам регистры, даем к ним доступ извне (конкретно, для класса Cookieboy::Memory). С видеопамятью и OAM чуть сложнее.
Как помните, ранее было сказано, что эта память доступна не всегда и зависит это от текущего состояния в регистре STAT. Появляется вопрос – реализовывать как надо (блокировать доступ к памяти в нужный момент) или как проще (память доступна всегда). Тут все зависит от того, насколько точна эмуляция переходов из состояния в состояние. Несоблюдение длительности интервалов может привести к тому, что память будет недоступна для чтения и записи тогда, когда должна быть доступна. В моей реализации оказалось возможным блокировать доступ – протестированные мной игры отлично работают в таких условиях. На ранних этапах моего проекта эмуляция LCD-контроллера была скудной (все делал как написано в Gameboy CPU Manual), и блокирование доступа приводило к выводу мусора на экране. Не все эмуляторы это делают, но решать вам.
Так же нам необходим буфер кадра, а можно даже два, как я и сделал. Один буфер кадра состоит из индексов цветов – этот буфер необходим, поскольку те же спрайты требуют проверки цвета под ними для правильного вывода. Второй буфер содержит копию того, что находится в первом буфере, но цвета уже представлены в RGB значениях для дальнейшего вывода на экран.
Для вывода графики нам необходимо завести две функции. Одну для вывода текущей строки (LY) на экран, складывающуюся из фона, «окна» и спрайтов — void Cookieboy::GPU::RenderScanline(). Другую для формирования очереди спрайтов для последующего вывода — void Cookieboy::GPU::PrepareSpriteQueue(). В разделе про логику LCD-контроллера я уже упоминал, куда нужно поместить вызовы этих функций.
Собственно, говорить о реализации этих функций нечего – либо говорить все от и до (так, конечно же, неинтересно), либо ничего не говорить. Тут у вас есть свобода действий – ваша задача лишь получить на экране правильную картинку. Какими путями вы к этому придете уже не важно. По исходному коду различных эмуляторов это заметно – подходы везде совершенно разные, в то время как другие компоненты имеют чуть ли не идентичную реализацию.
Подведем итоги. На данный момент наш эмулятор может исполнять код и даже выводить изображение на экран. Это все хорошо, но мы все еще не можем запускать тестовые ROM’ы для оценки проделанной работы. Большинство тестов подсчитывают время, а для этого нам надо реализовать еще один компонент DMG – таймеры.
Таймеры
Таймеры реализованы в виде двух счетчиков, работающих на определенных частотах.Первый счетчик, который мы будем называть DIV, работает на частоте 16384 Гц (т.е. отсчет происходит каждые 256 тактов процессора). Как принцип, так и его реализация предельно просты. С указанной частотой происходит инкрементирование значения в регистре DIV. DIV зациклен от 0 до 255. Т.е. если его значение равно 255, то следующий отсчет установит в нем значение 0. Если попытаться записать в DIV что-то самостоятельно, то его значение будет обнулено.
Второй счетчик, TIMA, немного сложнее и имеет больше возможностей. В общей сложности под него отведено три регистра:
- TIMA хранит текущее значение счетчика. Он так же зациклен, но при превышении 255 поведение совершенно иное, нежели у DIV. В этот момент запрашивается прерывание таймера, а в качестве значения TIMA устанавливается не 0, а значение из регистра TMA.
- TMA хранит начальное значение счетчика TIMA при превышении 255.
- TAC является управляющим регистром. Ниже приведена его структура.
Биты | Назначение |
2 | Остановка таймера: 0 – таймер остановлен 1 – таймер запущен |
0-1 | Частота таймера: 00 – 4096 Гц 01 – 262144 Гц 10 – 65536 Гц 11 – 16384 Гц |
Реализация практически такая же, как у DIV, только с учетом упомянутых особенностей. Здесь же стоит вспомнить об использовании while (кстати, здесь можно использовать более быстрый и не менее точный подход без цикла) как это было сделано с LCD-контроллером. При частоте 262144 Гц период составляет всего 16 тактов, а значит может возникнуть ситуация, когда за один раз нам придется сделать несколько отсчетов. Если счетчик будет равен двум и более периодам, то простое условие приведет к накоплению ошибки, из-за чего таймер будет работать неверно. На начальных этапах у меня были подобные проблемы.
И так, на данный момент у нас все готово для тестирования нашего эмулятора с помощью тестовых ROM’ов. Перед этим все же коснемся еще одной темы – управления.
Управление
![](https://habrastorage.org/storage2/037/321/ced/037321cedd6d335de9a8fb502320fc5c.jpg)
Как можно видеть на фотографии, DMG имеет 8 кнопок: 4-направленная крестовина, кнопки A и B, кнопки Start и Select. Чтобы игра могла узнать, какие кнопки нажаты в данный момент, DMG записывает пользовательский ввод в регистр P1. Вот его структура:
Бит | Назначение |
5 | P15 |
4 | P14 |
3 | P13 |
2 | P12 |
1 | P11 |
0 | P10 |
Смотря на таблицу, мы можем заметить, что битов на все кнопки здесь не хватает. В действительности, состояния кнопок записываются только в P13, P12, P11, P10. Биты P15 и P14 модифицируются ROM’ом игры для того, чтобы выбрать, состояние какой четверки клавиш мы хотим считывать через регистр P1. Вот таблица соответствия:
P15 | P14 | P13 | P12 | P11 | P10 |
1 | 0 | Вниз | Вверх | Влево | Вправо |
0 | 1 | Start | Select | B | A |
В этом регистре бит со значением 0 означает, что клавиша нажата. Если клавиша нажата, то запрашивается соответствующее прерывание. Именно это прерывание выводит DMG из состояния останова вследствие исполнения инструкции STOP. В таком состоянии основные компоненты не работают, и прерывания не запрашиваются. Только нажатие на кнопку может запросить его. В моем эмуляторе инструкция STOP игнорируется — игры работают нормально, а тестовые ROM'ы ее не проверяют.
Реализация сводится к опрашиванию состояния клавиш, которые эмулируют кнопки DMG, и установке соответствующих битов согласно значениям в P15 и P14. Но есть и свои нюансы.
Опрашивать физические клавиши (клавиатура, геймпад, не важно) каждый раз, скорее всего, не получится – производительность будет хуже некуда. Собственно, этого и не требуется. Лучше завести счетчик тактов и проверять состояние клавиш только по достижении им определенного значения. Какое? Решайте сами, я использовал длительность полного обновления экрана – 70224 такта. Этого достаточно для комфортной игры. Пока счетчик отмеряет время до следующего опроса клавиш, мы устанавливаем биты регистра P1 согласно полученным ранее состояниям клавиш.
Сводим все воедино
Последний штрих – поддержание необходимого темпа работы эмулятора. Поскольку одно полное обновление экрана длится 70224 такта, а частота процессора равна 4194304 Гц, DMG обновляет экран с частотой примерно 59.73 Гц, что для нас будет равно 60 Гц.В первую очередь нам нужна индикация того, что эмулятор закончил обновление экрана или отработал 70224 такта. Далее заводим цикл обновления, на каждый итерации которого наш эмулятор должен завершить одно обновление экрана. Параллельно с этим мы должны замерить время, которое уйдет у ПК на эмуляцию этого обновления экрана. Скорее всего, оно будет довольно маленьким и нам придется «усыпыть» приложение на некоторые время, чтобы в итоге одна итерация цикла заняла 1000/60 мс. Не забываем только вовремя опрашивать клавиатуру.
Именно здесь можно реализовать популярную в эмуляторах ускоренную эмуляцию, когда мы позволяем эмулятору работать на максимально возможной скорости.
И так, что мы имеем. На данный момент наш эмулятор содержит все функции, необходимые для запуска игр. Вся остальная функциональность является необязательной, но будет рассмотрена позже. Сейчас нам важно проверить корректность эмуляции с помощью тестовых ROM’ов.
Тестирование
Для DMG существует набор тестовых ROM'ов, которые проверяют различные компоненты DMG: процессор, память, звук, LCD. Для прохождения теста необходимо просто запустить ROM в своем эмуляторе. Результаты теста в наиболее удобном виде выводятся на экране. На данный момент мы можем провести тест на корректность работы инструкций процессора, тест длительности инструкций процессора, тест синхронизации процессора и памяти, тест LCD.Вот ссылка на оригиналы тестов и еще одна на всякий случай, мало ли что случится с сайтом автора, тесты очень и очень полезные. Помимо самих тестов по ссылкам можно найти их исходный код. Крайне рекомендую разобраться в ассемблере — понимание того, как и что тестируется, очень сильно поможет в исправлении ошибок. Сейчас это может быть не так важно, но в тестах звука без понимания исходного кода исправить ошибку иной раз будет попросту невозможно.
Тест процессора проверяет результат и флаги после исполнения каждой инструкции. В случае ошибки на экране будет выведен опкод неправильной инструкции. Данный пакет тестов состоит из 11 тестов различных групп инструкций. Именно отдельные тесты покажут, какие конкретно инструкций неправильно реализованы. Скорее всего, на данном этапе у вас будет достаточно много ошибок, в основном связанных с флагами. Больше всего проблем мне лично доставили POP AF, DAA и HALT. Правильную реализацию POP AF я уже упомянул – нет смысла наступать на те же грабли еще раз и вам, ничего кроме потери времени и интереса это не принесет. Мое негодование насчет настолько тривиальной инструкции не имело пределов, а всего-то надо было обнулить неиспользуемые биты в регистре F. DAA сложна сама по себе и вполне возможно будет проще и надежнее взять чью-то реализацию. HALT я так же упомянул во всех подробностях. Слепое следование CPU Manual привело к еще одной череде негодований…
Естественно не стоит полностью полагаться на тест – сам тестовый ROM может работать неправильно из-за неправильно реализованных инструкций или их длительности. Стоит сразу озаботиться о как можно более точной реализации всех аспектов DMG, чтобы с помощью тестов уже выявить только то, что вы не заметили или авторы забыли указать в документации.
Особенно это важно для тестов, связанных с тактами. Тест длительности инструкций процессора проверяет длительность каждой инструкции в тактах, в том числе, учитывая разницу в длительности у инструкций условных переходов. В Readme этого теста есть таблица с тактами.
Вполне возможно, что тест при запуске выдаст ошибку 255. Этой ошибки нет в исходном коде теста, отчего я вернулся к уже привычному для меня состоянию негодования, но после более тщательного анализа его кода стала понятна причина – тестовый ROM перед запуском проводит некоторые проверки и устанавливает таймеры для дальнейших измерений. Именно эти внутренние проверки не проходят. Для этого пришлось вооружиться таблицами из Readme, после чего тест успешно заработал и показал, где у меня еще закрались ошибки. В инструкциях условных переходов и было дело, практически все остальное было верно.
Тест синхронизации памяти и процессора проверяет состояние памяти в течение процесса исполнения инструкции процессора. Если вы вызываете функции синхронизации только после исполнения инструкции, то тест будет провален. Я уже упоминал о том, как сделать правильно, поэтому пройти этот тест уже не составляет особого труда. Если что, все необходимые данные тест выведет на экране. Если интересно как он работает, то все элементарно. Все операции производятся на счетчике таймера, установленного на максимальной частоте. При правильной синхронизации таймера с исполнением тестовой инструкции получится, что в процессе (именно в процессе) исполнения счетчик должен увеличиться. После исполнения эти значения и проверяются.
Наконец, тест LCD. Он проверяет, насколько точно соблюдается длительность состояний LCD-контроллера, и когда происходят различные события (например, увеличение счетчика строк LY). Без прохождения этого теста не стоит даже и задумываться о том, чтобы эмулировать блокировку видеопамяти и OAM в различных состояниях. Так же он проверит корректность вкл/выкл дисплея посредством 7 бита в регистре LCDC. Я уже упоминал, что манипуляции ROM’а с этим битом требуют определенных действий с нашей стороны.
Заключение
Могу вас поздравить — наш гипотетический эмулятор готов. Он содержит все критически важные функции, остальное реализовывать совсем не обязательно — игры будут прекрасно работать и сейчас.Играть можно, но без звука эмулятор все же выглядит неполноценным. Именно о звуке и пойдет речь в следующей статье. Тема эта большая, относительно сложная с кучей подводных камней, но очень и очень интересная.