Завершающая часть цикла. В этой главе рассмотрим работу с маппером MMC3 на примерах
<<< предыдущая
Источник
Раньше мы не использовали переключение банков памяти, но теперь настало время освоить маппер MMC3. Без маппера можно использовать 32 килобайта PRG ROM для кода и 8 килобайт CHR ROM для графики. Маппер позволяет обойти этот барьер.
Будем иметь в виду выпуск нашей игры на реальном картридже. Мануал http://kevtris.org/mappers/mmc3/ утверждает, что у нас есть такие варианты:
- До 64K PRG, 64K CHR
- До 512K PRG, 64K CHR
- До 512K PRG, VRAM
- До 512K PRG, 256K CHR
- До 128K PRG, 64K CHR, 8K CHR RAM
Список неполный. Выбираем самый компактный формат, 64/64к. Надо указать это в заголовке образа картриджа, чтобы эмулятор знал об этом. Документация на формат образа доступна в вики: http://wiki.nesdev.com/w/index.php/INES
.byte $4e,$45,$53,$1a
.byte $04 ; = 4 x 0х4000 байт PRG ROM
.byte $08 ; = 8 x 0х2000 байт CHR ROM
.byte $40 ; = маппер №4 - MMC3
Дальше надо прописать банки памяти в .cfg:
#Адреса банков ROM:
#все по адресу $8000, потому что они будут подставляться туда маппером
PRG0: start = $8000, size = $2000, file = %O ,fill = yes, define = yes;
PRG1: start = $8000, size = $2000, file = %O ,fill = yes, define = yes;
PRG2: start = $8000, size = $2000, file = %O ,fill = yes, define = yes;
PRG3: start = $8000, size = $2000, file = %O ,fill = yes, define = yes;
PRG4: start = $8000, size = $2000, file = %O ,fill = yes, define = yes;
PRG5: start = $a000, size = $2000, file = %O ,fill = yes, define = yes;
PRG6: start = $c000, size = $2000, file = %O ,fill = yes, define = yes;
PRG7: start = $e000, size = $1ffa, file = %O ,fill = yes, define = yes;
# Вектора прерываний в хвосте ROM
VECTORS: start = $fffa, size = $6, file = %O, fill = yes;
Все банки памяти будут подгружаться по одному и тому же адресу $8000. Исполняемый код будет в последнем неперегружаемом банке, и его можно разместить по любому адресу. Распределение памяти — самое сложное при работе с маппером, тут надо быть аккуратным.
Сегменты надо прописать в конфиге:
SEGMENTS {
HEADER: load = HEADER, type = ro;
CODE0: load = PRG0, type = ro, define = yes;
CODE1: load = PRG1, type = ro, define = yes;
CODE2: load = PRG2, type = ro, define = yes;
CODE3: load = PRG3, type = ro, define = yes;
CODE4: load = PRG4, type = ro, define = yes;
CODE5: load = PRG5, type = ro, define = yes;
CODE6: load = PRG6, type = ro, define = yes;
STARTUP: load = PRG7, type = ro, define = yes;
CODE: load = PRG7, type = ro, define = yes;
VECTORS: load = VECTORS, type = ro;
CHARS: load = CHR, type = rw;
BSS: load = RAM, type = bss, define = yes;
HEAP: load = RAM, type = bss, optional = yes;
ZEROPAGE: load = ZP, type = zp;
#OAM: load = OAM1, type = bss, define = yes;
}
Сегмент OAM в этом примере не используется.
А теперь запишем что-нибудь заметное в каждый банк и посмотрим, как оно разместится в ROM-файле. Для примера возьмем слова Bank0, Bank1 и так далее. Эти слова будут выводиться и на экран, переключение банков кнопкой Старт.
Размещение переменной в нужном банке делается через директиву PRAGMA:
#pragma rodata-name (“CODE0”)
#pragma code-name (“CODE0”)
const unsigned char TEXT1[]={
“Bank0”};
#pragma rodata-name (“CODE1”)
#pragma code-name (“CODE1”)
const unsigned char TEXT2[]={
“Bank1”};
#pragma rodata-name (“CODE2”)
#pragma code-name (“CODE2”)
const unsigned char TEXT3[]={
“Bank2”};
При нажатии Старт перключается банк памяти по адресам $8000-$9FFF, и первые 5 байт показываются на экране
void Draw_Bank_Num(void){ // функция вывода на экран
PPU_ADDRESS = 0x20;
PPU_ADDRESS = 0xa6;
for (index = 0;index < 5;++index){
PPU_DATA = TEXT1[index];
}
PPU_ADDRESS = 0;
PPU_ADDRESS = 0;
}
TEXT1 определяется на этапе компиляции и при старте консоли указывает в нулевой банк. При смене банка этот адрес останется неизменным, и в любом случае будет отображен текст из адресов $8000-8004. Банки переключаются вот так:
if (((joypad1old & START) == 0)&&((joypad1 & START) != 0)){
++PRGbank;
if (PRGbank > 7) PRGbank = 0;
*((unsigned char*)0x8000) = 6; // переключить банк PRG по адресу $8000
*((unsigned char*)0x8001) = PRGbank;
Draw_Bank_Num(); //вывод текста из нового банка
Адрес $8000 принадлежит ROM, но запись туда перехватывается маппером. Дальше указывается номер банка для подгрузки. Подробности как обычно в вики:
http://wiki.nesdev.com/w/index.php/MMC3
Немного путаницы вносит случайное равенство адресов начала банка и служебного регистра маппера. Мы можем перенести банк в адреса $A000-$BFFF:
*((unsigned char*)0x8000) = 7; // Адрес начала банка PRG - $A000
*((unsigned char*)0x8001) = which_PRG_bank;
Но регистры управления все равно остаются по адресам $8000 и $8001.
Я также добавил код инициализации в начало main(). Этот момент не документирован, но судя по всему, после RESET гарантирована правильная загрузка только последнего банка, по адресам $E000-$FFFF. Весь наш код инициализации должен располагаться только там.
Такая схема работы с банками памяти (когда их начало фиксировано по одному адресу) весьма неудобна. Обычно в начале каждого банка хранится массив с указателями на структуры данных и функции. Тогда можно переходить в них косвенными переходами, или более быстрым фокусом со стеком: http://wiki.nesdev.com/w/index.php/RTS_Trick. Там Ассемблер, но оно того стоит.
В любом случае, я хочу добавить прокрутку фона с параллаксом. Для этого надо каждые 4 кадра переключать банк CHR ROM в область памяти PPU — тайлы будут подхватываться оттуда. MMC3 разбивает CHR ROM на банки по 64 тайла, это 0x400 байт. Будем делать анимированный водопад, в каждом наборе тайлов они будут сдвинуты на 1 пиксель — при смене банков получится анимация.
Ссылка на исходный код, следующий кадр показывается по кнопке Старт:
Дропбокс
Гитхаб
Еще MMC3 умеет считать строки, выведенные на телевизор. Обычно это делается через нулевой спрайт, но он работает один раз за кадр — иногда нужно больше. Для имитации параллакса фона будем менять положение прокрутки каждые 20 строк. MMC3 будет вызывать прерывания в нужные моменты, и в его обработчике будет устанавливаться прокрутка в нужное положение. Обработчик написан на ассемблере, потому что при работе с С можно случайно повредить стек при вызове функции.
http://www.cc65.org/faq.php#IntHandlers
При старте приставки прерывания выключены, их надо включить в main().
asm (“cli”); // Включить прерывания
Указатели в векторе прерываний в конце файла reset.s должны показывать на правильные обработчики. Теперь можно настроить подсчет строк:
*((unsigned char*)0xe000) = 1; // Выключить MMC3 IRQ
*((unsigned char*)0xc000) = 20; // Вызвать прерывание через 20 строк
*((unsigned char*)0xc001) = 20;
*((unsigned char*)0xe001) = 1; // Снова включить MMC3 IRQ
Судя по всему, первая строка не учитывается, потому что прерывание срабатывает после 21 строки.
Еще очень желательно дергать горизонтальную прокрутку во время очень короткого периода H-blank — время хода луча на начало строки. Если это не учитывать, будет небольшое искажение изображения. Если знать куда смотреть, оно заметно во многих играх.
Прерывание MMC3 срабатывает ровно в H-blank, но его длительности не хватает на переход в обработчик. Так что я поставил там простой цикл, который ждет примерно 100 тактов до следующего H-blank. Этот момент может неточно обрабатываться некоторыми эмуляторами. Реальные игры не ждут следующей строки и делают сдвиг прокрутки в области с однотонной заливкой. После сдвига прокрутки ждем следующие 20 строк, и повторяем все снова.
Если хотите увидеть это своими глазами, исправьте ограничение цикла в обработчике. Сдвиг буквально 1 повторение будет видно — H-blank действительно настолько короткий.
Старт все еще переключает банки, но тут это не заметно.
Если лень возиться с перекомпиляцией, то вот гифка:
Цикл тайминга укорочен на 1 оборот — прокрутка меняется за несколько пикселей до конца строки. Искажение видно в правом конце нижней строки каждого горизонтального слоя. Оно меняется каждый кадр, так что на экране все пляшет. Если же прерывание сработает посреди строки, то будет совсем плохо.
Такая работа с прокруткой позволяет реализовать эффект параллакса. Запрос 'NES parallax scrolling' на Ютубе даст наглядные примеры. Опять-таки, обратите внимание, что в большинстве игр слои фона разделены однотонной заливкой.
Автор: Вадим Марков