Завершающая часть цикла. В этой главе рассмотрим работу с маппером MMC3 на примерах
<<< предыдущая

image
Источник

Раньше мы не использовали переключение банков памяти, но теперь настало время освоить маппер 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к. Надо указать это в заголовке образа картриджа, чтобы эмулятор знал об этом. Документация на формат образа доступна в вики:

Заголовок 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:

Фрагмент nes.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. Исполняемый код будет в последнем неперегружаемом банке, и его можно разместить по любому адресу. Распределение памяти — самое сложное при работе с маппером, тут надо быть аккуратным.

Сегменты надо прописать в конфиге:
nes.cfg
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:
lesson19.c
#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. Весь наш код инициализации должен располагаться только там.

Такая схема работы с банками памяти (когда их начало фиксировано по одному адресу) весьма неудобна. Обычно в начале каждого банка хранится массив с указателями на структуры данных и функции. Тогда можно переходить в них косвенными переходами, или более быстрым фокусом со стеком. Там Ассемблер, но оно того стоит.

В любом случае, я хочу добавить прокрутку фона с параллаксом. Для этого надо каждые 4 кадра переключать банк CHR ROM в область памяти PPU — тайлы будут подхватываться оттуда. MMC3 разбивает CHR ROM на банки по 64 тайла, это 0x400 байт. Будем делать анимированный водопад, в каждом наборе тайлов они будут сдвинуты на 1 пиксель — при смене банков получится анимация.

image

Ссылка на исходный код, следующий кадр показывается по кнопке Старт:
Дропбокс
Гитхаб

Еще 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 действительно настолько короткий.

image

Старт все еще переключает банки, но тут это не заметно.

Дропбокс
Гитхаб

Если лень возиться с перекомпиляцией, то вот гифка:

image

Цикл тайминга укорочен на 1 оборот — прокрутка меняется за несколько пикселей до конца строки. Искажение видно в правом конце нижней строки каждого горизонтального слоя. Оно меняется каждый кадр, так что на экране все пляшет. Если же прерывание сработает посреди строки, то будет совсем плохо.

Такая работа с прокруткой позволяет реализовать эффект параллакса. Запрос 'NES parallax scrolling' на Ютубе даст наглядные примеры. Опять-таки, обратите внимание, что в большинстве игр слои фона разделены однотонной заливкой.

Комментарии (1)


  1. ttyigor
    26.03.2018 12:23

    По ссылке под первой картинкой — 404 (https://forums.nesdev.com/viewtopic.php?f=2&t=16020)