Программирование для Famicom/NES/Денди в Nesicide+ca65: маппер MMC3 - перехват HBlank (9)
Итак, кроме собственно управления дополнительными банками памяти маппер MMC3 обладает еще одной важной функцией — генерацией прерываний IRQ по счётчику сканлайнов. В уроке про zero sprite hit мы перехватывали момент когда можно например в середине кадра сменить параметры прокрутки заднего фона этим средством встроенным в консоль. Но этот способ во первых можно использовать только один раз за кадр, а во вторых требует от процессора тратить все вычислительные ресурсы на обнаружение наступления события, что кроме самого этого факта еще и затрудняет планирование времени сколько код должен выполняться.
Счётчик сканлайнов в MMC3 лишён всех этих недостатков.
Как понятно из названия счётчик сканлайнов подсчитывает число рисуемых видеочипом сканлайнов и после определённого числа их может сгенерировать прерывание IRQ (это второе возможное прерывание кроме NMI которое у нас отслеживание наступление VBlank). Таким образом мы можем производить перехват HBlank и делать это несколько раз за кадр реализуя и сложные варианты параллакса и панель игровой информации. Т.к. эта возможность не была встроена в консоль, то инженерам пришлось реализовать её в маппере и интересно и важно то как именно это реализовано. Если в видеочипе включены одновременно и задний фон и спрайты и они настроены на разные таблицы изображений тайлов, то видеочип обращается к той и другой таблице тайлов попеременно сменяя обращения между банками $0XXX и $1XXX туда или сюда ровно 1 раз за сканлайн (важно заметить, что он будет считывать данные для спрайтов даже если все спрайты выведены за видимую область экрана и не видны). Таким образом линия A12 адресной шины видеочипа будет осциллировать ровно 1 раз за сканлайн и 241 раз за кадр (241, а не 240 видимо потому что есть один невидимый сканлайн перед началом кадра во время которого извлекаются данные для первых двух тайлов). Именно эти осцилляции и подсчитывает маппер MMC3 срабатывая на переходе A12 из 0 в 1 и делает это всегда — счётчик сканлайнов невозможно отключить. Точнее он перестанет считать если например выключить фон и спрайты (или даже что-то одно из них) или если они будут настроены на одну и ту же таблицу изображений тайлов. Но других способов отключить подсчёт нет (но как я покажу далее — и не надо). Более того — если мы что-то пишем в видеопамять в период VBlank через порты PPU_ADDR/DATA, то тоже вызовем срабатывание подсчёта если изменим линию A12, так что настраивать счётчик надо уже после работы с видеопамятью.
Если описать всю систему вкратце, то счётчик отсчитывает от значения которое мы записали в порт MMC3_IRQ_COUNTER до нуля и по достижению нуля если включены прерывания записью в порт MMC3_IRQ_ON, то генерируется прерывание IRQ и счётчик перезагружается значением из MMC3_IRQ_COUNTER. Напрямую счётчик для записи недоступен — только через значение в MMC3_IRQ_COUNTER, но можно лишь «спровоцировать» перезагрузку при следующем срабатывании счётчика записью в порт MMC3_IRQ_RELOAD.
Как это всё происходит по шагам: при каждом срабатывании (переходе A12 из 0 в 1) сперва проверяется равен ли счётчик нулю ИЛИ выставлен ли флаг перезагрузки записью любого значения в порт MMC3_IRQ_RELOAD — если любое из этих условий верно, то значение счётчика перезаписывается значением сохранённым в специальном регистре-защёлке записью байта в порт MMC3_IRQ_COUNTER. Иначе счётчик декрементируется.
Теперь проверяется равен ли счётчик нулю — и если да и прерывания разрешены записью любого значения в порт MMC3_IRQ_ON, то генерируется прерывание IRQ. Есть альтернативная версия маппера где генерация IRQ происходит при переходе счётчика из значения 1 в значение 0 по любой причине. Причём если мы обрабатываем прерывание, то важно записать в порт MMC3_IRQ_OFF иначе сигнал того что прерывание активировано не исчезнет с ножки процессора и он будет пытаться обрабатывать его постоянно.
Далее важно понять когда именно в сканлайне может генерироваться IRQ.
Каждый сканлайн длится 341 такт PPU (каждые 3 такта PPU в точности равны одному такту CPU). Каждый пиксель сканлайна рисуется за 1 такт PPU. При этом после 256 видимых пикселей экрана (0-255) как бы есть 85 «невидимых пикселя» (256-340).
Если фон настроен на таблицу изображений тайлов $0XXX, а спрайты — $1XXX, то счётчик сработает на пикселе 260, т.е. почти при начале HBlank.
Если наоборот фон настроен на таблицу изображений тайлов $1XXX, а спрайты — $0XXX, то счётчик сработает на пикселе 324, т.е. почти в конце HBlank и перед началом следующего сканлайна.
Тут еще важно отметить, что т.к. обработчику прерывания надо сперва сохранить регистры в стек, а эти инструкции растягиваются на десяток тактов CPU, что равно трём десяткам тактов PPU, т.е. этих вот виртуальных или реальных пикселей, а данные для первых тайлов следующего сканлайна видеочип начинает считывать уже на 320-ом такте, плюс обновляет регистры скроллинга тоже заранее, то даже если прерывание сработает в начале HBlank у нас по факту не остаётся достаточно времени чтобы корректно обновить регистры скроллинга. По крайней мере у меня не получилось. Посему как я понял чаще всего мы просто пропускаем достаточное число тактов до следующей строки чтобы вклиниться ровно там где нужно записями в те или иные регистры. Так что второй вариант срабатываний выглядит не таким уж и неинтересным.
Еще в связи со всем вышесказанным следует осторожничать с режимом спрайтов 8x16, т.к. он может переключать линию A12 если для спрайтов будут использоваться обе таблицы тайлов. Так лучше не делать, ибо учесть там всё слишком сложно. Если и использовать режим 8x16, то данные спрайтов указывать только из одной таблицы.
Ну что же, краткий теоретический курс молодого бойца со счётчиком сканлайнов MMC3 мы прошли, а теперь надо бы реализовать что-нибудь на практике.
Сперва я хотел в одном уроке охватить и переключение банков видеопамяти и смену скроллинга в строке, но понял что оба дела в одном будут слишком громоздким уроком. Поэтому в этой статье я рассмотрю переключение видеобанков и будет еще одна с полноценной подменой параметров скроллинга.
В видеочипе Famicom/NES/Денди есть принципиальное ограничение — в одном кадре разных тайлов того же фона на экране может быть только 256, а видимых клеток с тайлами на экране примерно 896. Т.е. ~70% тайлов фона должны повторяться, быть неуникальными.
Это ограничение приводит к забавным решениям о том как вывести на экране консоли графическую картинку максимально большого размера и у нас тут уже есть отличная статья от Shiru о терниях на этом пути.
Вот и мы вступим на эту тропу и попробуем с помощью переключения видеобанков с тайлами вывести на экран картинку где каждый пиксель может быть уникальным.
Мы заполним экранную область монотонно нарастающими номерами тайлов — т.к. их 32 в строке, то получится что через 8 строк тайлов (64 сканлайна) тайлы начнут повторяться. Но мы перед этим сканлайном переключим банк видеопамяти с изображениями тайлов плиток фона на видеостраницу с другими изображениями тайлов и сделав это 3 раза за кадр получим полноценную картинку.
Т.к. вся видеостраница в памяти имеет размер 32x30 тайлов (включая невидимые строки), то теоретически нам понадобится картинка размером 256x240 пикселей. Однако для простоты последующей копипасты в редакторе и маппингу всего в килобайтные банки видеопамяти я сделал её размером 256x256. Четырёхцветную для максимальной простоты — будем использовать одну палитру на все знакоместа экрана.
Итак, вот какую картинку я создал в GIMP:
Как мы помним видеоданные в Nesicide (по умолчанию) представлены порциями по 256 тайлов в формате картинок 128x128 пикселей.
Таким образом нам надо сохранить картинку 256x256 в четыре картинки 128x128.
Но возникает еще вопрос как это сделать. Если тайлы выстраивать линейно с нарастанием номеров слева-направо сверху-вниз, то придётся нарезать картинку в слои высотой 8 пикселей, это будет долго и нудно.
Поэтому нарезку я произвёл другим способом, вот так:
Тут преследуются две цели: во первых чтобы последовательные восемь строк лежали целиком в одном банке (так меньше переключений нужно сделать банков), а во вторых чтобы поменьше надо было вырезать-вставлять эти блоки в графическом редакторе.
В связи с этим замощение видеостраницы номерами тайлов стало не совсем линейным — в банках то нумерация идёт линейно от 0 до 255 слева-направо и сверху-вниз, но на экране будут перемежаться как бы две колонки высотой в четыре тайла.
Сперва тайлы 0-15 левой половины, потом 64-79 правой.
Потом 16-31 слева и 80-95 справа.
32-47 слева и 96-111 справа.
48-63 слева и 112-127 справа.
Выглядит замудрёно, но что тут происходит — мы левую и правую половины экрана заливаем последовательно слева-направо сверху-вниз, но левая половина экрана отсчитывает от 0, а правая — от 64.
Следующие 4 строки заполняются индексами тайлов так же, но левая половина уже стартует от 128, а правая — от 192.
Этот паттерн нужно 4 раза повторить на экране (последний раз не влезет на 2 строки которых нет) и так мы сопоставим тайлы в соответствие с пикселями банков видеоданных в исходную картинку.
Ну что же, задача поставлена — начинаем делать новый проект или берём готовый Example06 из архива yadi.sk/d/_THxg1gxuCCVNw
Создадим новый проект на базе предыдущего (Example05). Так все файлы в папке src будут такими же кроме main.s. nes.ini тоже будет совершенно таким же.
Однако в Example05 мы наводнили 64Кб графических данных клонами двух картинок, здесь же мы создадим 16 уникальных графических файла:
В первые два банка мы назначим то же самое что в предыдущем уроке было в файлах gamegfx.png и titlegfx.png соответственно (причём в данном примере они не используются, так что вообще неважно что в них находится, я немного запарился пытаясь в этот один урок втиснуть два и думал мне пригодится алфавит, но он будет нужен в следующем уроке, а в этом бесполезен, так что содержимое всех файлов кроме ниже обозначенных неважно, главное чтобы они были картинками 128x128 4bpp).
А вот в банки tiles01A.png, tiles01B.png, tiles02A.png и tiles02B.png мы назначим четыре картинки 128x128 что мы создали выше.
MMC3 дробит видеоROM картриджа на страницы размером 1Кб, так что в каждом таком файле по порядку располагается 4 банка графики MMC3.
Значит первые два неиспользованных нами файла это страницы графики 0-7, а наши четыре банка это страницы 8-11, 12-15, 16-19, 20-23 соответственно.
Ну что же, заполнив графические данные ROM приступим писать код. Всё отличие с предыдущим уроком тут будет только в одном файле — main.s:
main.s — начало
Тут всё почти так же как в предыдущем уроке кроме того, что единственной новой переменной — next_page и того факта что нам неинтересны дополнительные сегменты кода в ROM PRG и мы оставили их пустыми работая сразу в ROM_H.
Новый код идёт дальше:
main.s — вторая часть
Это процедуры для заполнения страницы видеопамяти четыре раза подряд таким паттерном из номеров тайлов чтобы он правильно отобразился на данные графики тайлов. Всё это я описывал выше, тут просто реализация в коде.
main.s — третья часть, обработка IRQ
Это самая мякотка этого урока — здесь перехватчик прерывания IRQ впервые в наших уроках не пуст, а выполняет перехват HBlank и подменяет банки с графическими данными CHR на лету.
В коде инициализации этого урока ниже маппинг таблицы изображений тайлов в $0XXX PPU откуда берёт данные видеочип для фона будут выставлены в режим половинок — Halves — таким образом мы переключаем сразу банками по 2 Кб записью чётных страниц в порты MMC3_CHR_Hx.
Самое главное что нам нужно выдержать правильную паузу циклом по регистру процессора X чтобы попасть в нужный интервал уже следующей строки на экране, т.к. в той где прерывание сработало мы уже не успеваем. Каждое новое приложение с новым функционалом и новым кодом в обработчике прерывания должен требовать своей подстройки этого параметра — более подробно об этом будет следующий урок.
Идём дальше — инициализация приложения:
main.s — четвёртая часть, инициализация
Здесь мы выставляем всё как и договаривались. Главное включить и спрайты и фон, развести из по разным банках данных для тайлов ($0XXX для фона и $0XXX для спрайтов) и вывести спрайты за пределы экрана чтобы они были не видны ну и вызвать процедуру заполнения экранной области нужными индексами тайлов и индексов палитр.
Обязательно надо выполнить инструкцию cli, т.к. прерывания IRQ маскируемые, т.е. их обработку может сам себе запретить процессор исполнив инструкцию sei — тогда он просто не будет на них реагировать. А ведь эта инструкция — первая что исполняет наша программа при старте. В отличие от прерывания NMI от которого процессор может «отделаться» только записью во внешние порты тех устройств что их могут генерировать и рассчитаны на возможность запрета тут сам процессор может игнорировать прерывание если выставит этот бит запрета обработки прерывания IRQ в регистре флагов инструкцией sei. Поэтому надо явно разрешить эту обработку инструкцией cli.
Последняя часть кода — игровой цикл:
main.s — последняя часть, игровой цикл
И вот тут всё очень кратко — сперва заполняем таблицу спрайтов так чтобы все они были не видны выводом координаты Y за границы экрана, потом маппим текущие данные тайлов для фона на страницы 8-11 (опять таки помним, что мы тут используем маппинг таблицы тайлов в режиме «половинок», поэтому указываются только чётные два адреса — нечеты выставятся самим чипом). А в переменную next_page сохраним число 12 — это номер начала следующих банков куда надо будет выставить маппинг видеостраниц по прерыванию от счётчика сканлайнов MMC3.
Далее важная вещь — мы еще находимся в VBlank и добрались до места где счётчик сканлайнов уже нельзя будет «неправильно возбудить» записью в видеопамять. Здесь мы записью (любого значения) в порт MMC3_IRQ_RELOAD выставляем флаг того что при следующем срабатывании счётчика (а это будет первый «невидимый» сканлайн кадра который последует за VBlank) его надо перезагрузить значением 63 и сперва записываем это значение в порт MMC3_IRQ_COUNTER. Так мы гарантируем, что счётчик чем бы он сейчас ни был станет в начале кадра чем ему нужно быть.
Ну и как бы всё! Записываем любое значение в порт MMC3_IRQ_ON чтобы включить генерацию прерываний и наслаждаемся результатом и здесь не будет видео, т.к. здесь нет ничего динамичного — просто картинка, но вот скриншот:
Итак, перехват прерывания по сканлайну в MMC3 намного более гибкая штука чем «ушибление спрайта», но в следующем уроке я еще покажу как правильно скроллить с её помощью игровое поле во всех двух игровых измерениях сохраняя неподвижную «панель состояния» в сверху или снизу экрана и это будет последний урок в этом цикле.
В первую часть (оглавление)...
Счётчик сканлайнов в MMC3 лишён всех этих недостатков.
Теория
Как понятно из названия счётчик сканлайнов подсчитывает число рисуемых видеочипом сканлайнов и после определённого числа их может сгенерировать прерывание IRQ (это второе возможное прерывание кроме NMI которое у нас отслеживание наступление VBlank). Таким образом мы можем производить перехват HBlank и делать это несколько раз за кадр реализуя и сложные варианты параллакса и панель игровой информации. Т.к. эта возможность не была встроена в консоль, то инженерам пришлось реализовать её в маппере и интересно и важно то как именно это реализовано. Если в видеочипе включены одновременно и задний фон и спрайты и они настроены на разные таблицы изображений тайлов, то видеочип обращается к той и другой таблице тайлов попеременно сменяя обращения между банками $0XXX и $1XXX туда или сюда ровно 1 раз за сканлайн (важно заметить, что он будет считывать данные для спрайтов даже если все спрайты выведены за видимую область экрана и не видны). Таким образом линия A12 адресной шины видеочипа будет осциллировать ровно 1 раз за сканлайн и 241 раз за кадр (241, а не 240 видимо потому что есть один невидимый сканлайн перед началом кадра во время которого извлекаются данные для первых двух тайлов). Именно эти осцилляции и подсчитывает маппер MMC3 срабатывая на переходе A12 из 0 в 1 и делает это всегда — счётчик сканлайнов невозможно отключить. Точнее он перестанет считать если например выключить фон и спрайты (или даже что-то одно из них) или если они будут настроены на одну и ту же таблицу изображений тайлов. Но других способов отключить подсчёт нет (но как я покажу далее — и не надо). Более того — если мы что-то пишем в видеопамять в период VBlank через порты PPU_ADDR/DATA, то тоже вызовем срабатывание подсчёта если изменим линию A12, так что настраивать счётчик надо уже после работы с видеопамятью.
Если описать всю систему вкратце, то счётчик отсчитывает от значения которое мы записали в порт MMC3_IRQ_COUNTER до нуля и по достижению нуля если включены прерывания записью в порт MMC3_IRQ_ON, то генерируется прерывание IRQ и счётчик перезагружается значением из MMC3_IRQ_COUNTER. Напрямую счётчик для записи недоступен — только через значение в MMC3_IRQ_COUNTER, но можно лишь «спровоцировать» перезагрузку при следующем срабатывании счётчика записью в порт MMC3_IRQ_RELOAD.
Как это всё происходит по шагам: при каждом срабатывании (переходе A12 из 0 в 1) сперва проверяется равен ли счётчик нулю ИЛИ выставлен ли флаг перезагрузки записью любого значения в порт MMC3_IRQ_RELOAD — если любое из этих условий верно, то значение счётчика перезаписывается значением сохранённым в специальном регистре-защёлке записью байта в порт MMC3_IRQ_COUNTER. Иначе счётчик декрементируется.
Теперь проверяется равен ли счётчик нулю — и если да и прерывания разрешены записью любого значения в порт MMC3_IRQ_ON, то генерируется прерывание IRQ. Есть альтернативная версия маппера где генерация IRQ происходит при переходе счётчика из значения 1 в значение 0 по любой причине. Причём если мы обрабатываем прерывание, то важно записать в порт MMC3_IRQ_OFF иначе сигнал того что прерывание активировано не исчезнет с ножки процессора и он будет пытаться обрабатывать его постоянно.
Далее важно понять когда именно в сканлайне может генерироваться IRQ.
Каждый сканлайн длится 341 такт PPU (каждые 3 такта PPU в точности равны одному такту CPU). Каждый пиксель сканлайна рисуется за 1 такт PPU. При этом после 256 видимых пикселей экрана (0-255) как бы есть 85 «невидимых пикселя» (256-340).
Если фон настроен на таблицу изображений тайлов $0XXX, а спрайты — $1XXX, то счётчик сработает на пикселе 260, т.е. почти при начале HBlank.
Если наоборот фон настроен на таблицу изображений тайлов $1XXX, а спрайты — $0XXX, то счётчик сработает на пикселе 324, т.е. почти в конце HBlank и перед началом следующего сканлайна.
Тут еще важно отметить, что т.к. обработчику прерывания надо сперва сохранить регистры в стек, а эти инструкции растягиваются на десяток тактов CPU, что равно трём десяткам тактов PPU, т.е. этих вот виртуальных или реальных пикселей, а данные для первых тайлов следующего сканлайна видеочип начинает считывать уже на 320-ом такте, плюс обновляет регистры скроллинга тоже заранее, то даже если прерывание сработает в начале HBlank у нас по факту не остаётся достаточно времени чтобы корректно обновить регистры скроллинга. По крайней мере у меня не получилось. Посему как я понял чаще всего мы просто пропускаем достаточное число тактов до следующей строки чтобы вклиниться ровно там где нужно записями в те или иные регистры. Так что второй вариант срабатываний выглядит не таким уж и неинтересным.
Еще в связи со всем вышесказанным следует осторожничать с режимом спрайтов 8x16, т.к. он может переключать линию A12 если для спрайтов будут использоваться обе таблицы тайлов. Так лучше не делать, ибо учесть там всё слишком сложно. Если и использовать режим 8x16, то данные спрайтов указывать только из одной таблицы.
Ну что же, краткий теоретический курс молодого бойца со счётчиком сканлайнов MMC3 мы прошли, а теперь надо бы реализовать что-нибудь на практике.
Сперва я хотел в одном уроке охватить и переключение банков видеопамяти и смену скроллинга в строке, но понял что оба дела в одном будут слишком громоздким уроком. Поэтому в этой статье я рассмотрю переключение видеобанков и будет еще одна с полноценной подменой параметров скроллинга.
Задача
В видеочипе Famicom/NES/Денди есть принципиальное ограничение — в одном кадре разных тайлов того же фона на экране может быть только 256, а видимых клеток с тайлами на экране примерно 896. Т.е. ~70% тайлов фона должны повторяться, быть неуникальными.
Это ограничение приводит к забавным решениям о том как вывести на экране консоли графическую картинку максимально большого размера и у нас тут уже есть отличная статья от Shiru о терниях на этом пути.
Вот и мы вступим на эту тропу и попробуем с помощью переключения видеобанков с тайлами вывести на экран картинку где каждый пиксель может быть уникальным.
Мы заполним экранную область монотонно нарастающими номерами тайлов — т.к. их 32 в строке, то получится что через 8 строк тайлов (64 сканлайна) тайлы начнут повторяться. Но мы перед этим сканлайном переключим банк видеопамяти с изображениями тайлов плиток фона на видеостраницу с другими изображениями тайлов и сделав это 3 раза за кадр получим полноценную картинку.
Т.к. вся видеостраница в памяти имеет размер 32x30 тайлов (включая невидимые строки), то теоретически нам понадобится картинка размером 256x240 пикселей. Однако для простоты последующей копипасты в редакторе и маппингу всего в килобайтные банки видеопамяти я сделал её размером 256x256. Четырёхцветную для максимальной простоты — будем использовать одну палитру на все знакоместа экрана.
Итак, вот какую картинку я создал в GIMP:
Как мы помним видеоданные в Nesicide (по умолчанию) представлены порциями по 256 тайлов в формате картинок 128x128 пикселей.
Таким образом нам надо сохранить картинку 256x256 в четыре картинки 128x128.
Но возникает еще вопрос как это сделать. Если тайлы выстраивать линейно с нарастанием номеров слева-направо сверху-вниз, то придётся нарезать картинку в слои высотой 8 пикселей, это будет долго и нудно.
Поэтому нарезку я произвёл другим способом, вот так:
Тут преследуются две цели: во первых чтобы последовательные восемь строк лежали целиком в одном банке (так меньше переключений нужно сделать банков), а во вторых чтобы поменьше надо было вырезать-вставлять эти блоки в графическом редакторе.
В связи с этим замощение видеостраницы номерами тайлов стало не совсем линейным — в банках то нумерация идёт линейно от 0 до 255 слева-направо и сверху-вниз, но на экране будут перемежаться как бы две колонки высотой в четыре тайла.
Сперва тайлы 0-15 левой половины, потом 64-79 правой.
Потом 16-31 слева и 80-95 справа.
32-47 слева и 96-111 справа.
48-63 слева и 112-127 справа.
Выглядит замудрёно, но что тут происходит — мы левую и правую половины экрана заливаем последовательно слева-направо сверху-вниз, но левая половина экрана отсчитывает от 0, а правая — от 64.
Следующие 4 строки заполняются индексами тайлов так же, но левая половина уже стартует от 128, а правая — от 192.
Этот паттерн нужно 4 раза повторить на экране (последний раз не влезет на 2 строки которых нет) и так мы сопоставим тайлы в соответствие с пикселями банков видеоданных в исходную картинку.
Ну что же, задача поставлена — начинаем делать новый проект или берём готовый Example06 из архива yadi.sk/d/_THxg1gxuCCVNw
Проект Example06
Создадим новый проект на базе предыдущего (Example05). Так все файлы в папке src будут такими же кроме main.s. nes.ini тоже будет совершенно таким же.
Однако в Example05 мы наводнили 64Кб графических данных клонами двух картинок, здесь же мы создадим 16 уникальных графических файла:
- tiles00A.png
- tiles00B.png
- tiles01A.png
- tiles01B.png
- tiles02A.png
- tiles02B.png
- tiles03A.png
- tiles03B.png
- tiles04A.png
- tiles04B.png
- tiles05A.png
- tiles05B.png
- tiles06A.png
- tiles06B.png
- tiles07A.png
- tiles07B.png
В первые два банка мы назначим то же самое что в предыдущем уроке было в файлах gamegfx.png и titlegfx.png соответственно (причём в данном примере они не используются, так что вообще неважно что в них находится, я немного запарился пытаясь в этот один урок втиснуть два и думал мне пригодится алфавит, но он будет нужен в следующем уроке, а в этом бесполезен, так что содержимое всех файлов кроме ниже обозначенных неважно, главное чтобы они были картинками 128x128 4bpp).
А вот в банки tiles01A.png, tiles01B.png, tiles02A.png и tiles02B.png мы назначим четыре картинки 128x128 что мы создали выше.
MMC3 дробит видеоROM картриджа на страницы размером 1Кб, так что в каждом таком файле по порядку располагается 4 банка графики MMC3.
Значит первые два неиспользованных нами файла это страницы графики 0-7, а наши четыре банка это страницы 8-11, 12-15, 16-19, 20-23 соответственно.
Ну что же, заполнив графические данные ROM приступим писать код. Всё отличие с предыдущим уроком тут будет только в одном файле — main.s:
main.s — начало
; Подключаем заголовок библиотеки Famicom/NES/Денди
.include "src/neslib.inc"
; Подключаем заголовок библиотеки маппера MMC3
.include "src/mmc3.inc"
; Сегмент векторов прерываний и сброса/включения - находится в самых
; последних шести байтах адресного пространства процессора ($FFFA-FFFF)
; и содержит адреса по которым процессор переходит при наступлении события
.segment "VECTORS"
.addr nmi ; Вектор прерывания NMI (процедура nmi ниже)
.addr reset ; Вектор сброса/включения (процедура reset ниже)
.addr irq ; Вектор прерывания IRQ (процедура irq ниже)
.segment "ZPAGE": zp ; Сегмент zero page, это надо пометить через ": zp"
vblank_counter: .byte 0 ; Счётчик прерываний VBlank
next_page: .byte 0 ; Номер следующей видеостраницы к переключению
.segment "RAM" ; Сегмент неинициализированных данных в RAM
.segment "ROM_0" ; Страница данных 0 (первые 8Кб из 64 ROM картриджа) для адреса $8000
.segment "ROM_1" ; Страница данных 1 (вторые 8Кб из 64 ROM картриджа) для адреса $8000
.segment "ROM_2" ; Страница данных 2...
.segment "ROM_3" ; Страница данных 3...
.segment "ROM_4" ; Страница кода 4 (пятые 8Кб из 64 ROM картриджа) для адреса $A000
.segment "ROM_5" ; Страница кода 5 (шестые 8Кб из 64 ROM картриджа) для адреса $A000
; С MMC3 в сегменте ROM_H у нас располагаются последние страницы ROM картриджа
; т.е. в данной конфигурации с 64Кб ROM - 6 и 7 по порядку.
.segment "ROM_H" ; Сегмент данных в ПЗУ картриджа (страницы $C000-$FFFF)
palettes: ; Подготовленные наборы палитр (для фона и для спрайтов)
; Повторяем наборы 2 раза - первый для фона и второй для спрайтов
.repeat 2
.byte $0F, $0A, $1A, $2A ; черный, тёмно-зеленый, зеленый, светло-зеленый
.byte $0F, $16, $1A, $11 ; -, красный, зеленый, синий
.byte $0F, $1A, $11, $16 ; -, зеленый, синий, красный
.byte $0F, $11, $16, $1A ; -, синий, красный, зеленый
.endrep
; nmi - процедура обработки прерывания NMI
; Обрабатывает наступление прерывания VBlank от PPU (см. процедуру wait_nmi)
.proc nmi
inc vblank_counter ; Просто увеличим vblank_counter
rti ; Возврат из прерывания
.endproc
; wait_nmi - ожидание наступления прерывания VBlank от PPU
; Согласно статье https://wiki.nesdev.com/w/index.php/NMI ожидание VBlank
; опросом верхнего бита PPU_STATUS в цикле может пропускать целые кадры из-за
; специфической гонки состояний, поэтому правильнее всего перехватывать прерывание,
; в нём наращивать счётчик (процедура nmi выше) и ожидать его изменения как в коде ниже.
.proc wait_nmi
lda vblank_counter
notYet: cmp vblank_counter
beq notYet
rts
.endproc
; fill_palettes - заполнить все наборы палитр данными из адреса в памяти
; вход:
; arg0w - адрес таблицы с набором палитр (2 * 4 * 4 байта)
.proc fill_palettes
fill_ppu_addr $3F00 ; палитры в VRAM находятся по адресу $3F00
ldy # 0 ; зануляем счётчик и одновременно индекс
loop:
lda (arg0w), y ; сложный режим адресации - к слову лежащему в zero page
; по однобайтовому адресу arg0w прибавляется Y и
; в A загружается байт из полученного адреса
sta PPU_DATA ; сохраняем в VRAM
iny ; инкрементируем Y
cpy # 2 * 4 * 4 ; проверяем на выход за границу цикла
bne loop ; и зацикливаемся если она еще не достигнута
rts ; выходим из процедуры
.endproc
; fill_attribs - заполнить область цетовых атрибутов байтом в аккумуляторе
; адрес в PPU_ADDR уже должен быть настроен на эту область атрибутов!
.proc fill_attribs
ldx # 64 ; надо залить 64 байта цветовых атрибутов
loop: sta PPU_DATA ; записываем в VRAM аккумулятор
dex ; декрементируем X
bne loop ; цикл по счётчику в X
rts ; возврат из процедуры
.endproc
Тут всё почти так же как в предыдущем уроке кроме того, что единственной новой переменной — next_page и того факта что нам неинтересны дополнительные сегменты кода в ROM PRG и мы оставили их пустыми работая сразу в ROM_H.
Новый код идёт дальше:
main.s — вторая часть
; fill_4_lines - заполнить в чересполосицу 8 строк страницы видеопамяти
; (указатель в PPU_ADDR уже должен быть настроен как надо)
; вход:
; arg0b - начальный индекс с которого надо заполнять левую половину экрана
; arg1b - начальный индекс с которого надо заполнять правую половину экрана
.proc fill_4_lines
store arg2b, # 4 ; в arg2b сохраним счётчик цикла по строкам (4 раза)
loop:
ldx arg0b ; для левой половины строки экрана берём индекс тайлов в arg0b
ldy # 16 ; 16 раз надо повторить цикл по строке слева...
loop1: stx PPU_DATA ; записываем текущий индекс тайла в экранную область
inx ; и увеличиваем его.
dey ; уменьшаем счётчик цикла в Y
bne loop1 ; и если он 0
stx arg0b ; сохраняем увеличенный индекс тайла обратно в arg0b
ldx arg1b ; теперь идёт правая половина строки на экране
ldy # 16 ; опять 16 раз надо повторить
loop2: stx PPU_DATA ; запись в экранную область индекса тайла
inx ; и его инкремент.
dey ; опять уменьшаем счётчик цикла в Y
bne loop2 ; и повторяем итерацию если он не достиг нуля
stx arg1b ; сохраняем индеса тайла для правой половины экрана в arg1b
; здесь мы достигли следующей строки (залили 32 байта)
dec arg2b ; уменьшаем счётчик цикла по строкам
bne loop ; и если он не достиг нуля - продолжаем цикл
rts ; возвращаемся из процедуры
.endproc
; fill_8_lines - заполнить 8 строк экрана нарастающими в правой и в левой половинах
; экрана индексами тайлов так чтобы покрыть все 256 возможных индексов тайлов
; заполнив 8 строк экрана (8*32=256) таким паттерном чтобы вывести битмап 256x64 пикселя
; из MMC3_CHR_H0 и MMC3_CHR_H1 как мы сохранили в тайлсете.
.proc fill_8_lines
; тайлсет упорядочен таким образом, что битмап состоит из четырёх
; областей разбросанных справа и слева на экране в четыре области которые надо
; вывести в два захода по 4 строки за раз процедурой fill_4_lines:
store arg0b, # 0 ; первая левая четверть начинается с тайла 0
store arg1b, # 64 ; первая правая четверть начинается с тайла 64
jsr fill_4_lines ; выводим первые 4 строки
store arg0b, # 128 ; вторая левая четверть начинается с тайла 128
store arg1b, # 192 ; вторая правая четверть начинается с тайла 192
jsr fill_4_lines ; выводим вторые 4 строки
rts ; возвращаемся из процедуры.
.endproc
; fill_32_lines - вызовем четыре раза подряд fill_8_lines
.proc fill_32_lines
jsr fill_8_lines
jsr fill_8_lines
jsr fill_8_lines
jsr fill_8_lines
rts
.endproc
Это процедуры для заполнения страницы видеопамяти четыре раза подряд таким паттерном из номеров тайлов чтобы он правильно отобразился на данные графики тайлов. Всё это я описывал выше, тут просто реализация в коде.
main.s — третья часть, обработка IRQ
; irq - процедура обработки прерывания IRQ.
; Она вызывается при наступлении прерывания от MMC3, т.е. по счётчику строк.
; Сперва в теле основного цикла в VBlank мы настроим отображение страниц на начало
; данных битмапа в тайлсете.
; Далее при выводе кадра начнут отсчитываться сканлайны и данная процедура вызовется
; 3 раза - и в ней мы будем сменять отображение страниц в данные тайлов (CHR) и
; тем самым будем менять тайлсет на лету чтобы на экране сформировалась большая
; картинка из уникальных пикселей.
; На входе в next_page хранится номер банка видеоданных на который надо переключиться.
.proc irq
pha ; сохраняем аккумулятор в стек
txa ; помещаем X в A
pha ; сохраняем снова A (т.е. X) в стек
; выключаем прерывание MMC3 и одновременно этим сбрасываем флаг
; наступившего прерывания, иначе прерывание будет генерироваться
; каждый сканлайн!
sta MMC3_IRQ_OFF
; при данной конфигурации видео (из какой половины CHR берутся данные
; для фона, а из какой - для спрайтов) прерывание срабатывает в самом конце
; строки в её периоде HBlank и достаточно поздно чтобы мы в этот HBlank уже
; могли обновлять параметры видео без видимых глюков.
; поэтому нам придётся подождать следующего HBlank искуственной паузой
ldx # 15 ; меняя количество холостых циклов на 10 или 20 вы
loop: dex ; можете увидеть как с разных концов экрана будут
bne loop ; появляться глюки
; сменим банк тайловых данных в первой половине CHR на новый:
mmc3_set_bank_page # MMC3_CHR_H0, next_page
; с этой точки точные тайминги уже не критичны, т.к. видеочип
; будет занят отрисовкой уже настроенной первой половины CHR
inc next_page ; увеличим текущий банк на два, т.к. мы
inc next_page ; работаем в режиме половинок, а не четвертей.
; и выставим следующий банк в CHR_H1:
mmc3_set_bank_page # MMC3_CHR_H1, next_page
inc next_page ; и опять увеличим текущий банк графики
inc next_page ; на две четверти вперёд
; Следущее прерывание должно сработать через 64 строки далее, но т.к.
; мы ждали пропуска строки, то надо загрузить в счётчик на 1 меньше - 63
store MMC3_IRQ_COUNTER, # 63
sta MMC3_IRQ_ON ; включим прерывания MMC3
pla ; восстановим A из стека
tax ; и скопируем в X, т.к. это был он
pla ; а теперь восстановим A
rti ; Инструкция возврата из прерывания
.endproc
Это самая мякотка этого урока — здесь перехватчик прерывания IRQ впервые в наших уроках не пуст, а выполняет перехват HBlank и подменяет банки с графическими данными CHR на лету.
В коде инициализации этого урока ниже маппинг таблицы изображений тайлов в $0XXX PPU откуда берёт данные видеочип для фона будут выставлены в режим половинок — Halves — таким образом мы переключаем сразу банками по 2 Кб записью чётных страниц в порты MMC3_CHR_Hx.
Самое главное что нам нужно выдержать правильную паузу циклом по регистру процессора X чтобы попасть в нужный интервал уже следующей строки на экране, т.к. в той где прерывание сработало мы уже не успеваем. Каждое новое приложение с новым функционалом и новым кодом в обработчике прерывания должен требовать своей подстройки этого параметра — более подробно об этом будет следующий урок.
Идём дальше — инициализация приложения:
main.s — четвёртая часть, инициализация
; reset - стартовая точка всей программы - диктуется вторым адресом в сегменте
; VECTORS оформлена как процедура, но вход в неё происходит при включении консоли
; или сбросу её по кнопке RESET, поэтому ей некуда "возвращаться" и она
; принудительно инициализирует память и стек чтобы работать с чистого листа.
.proc reset
; ***********************************************************
; * Первым делом нужно привести систему в рабочее состояние *
; ***********************************************************
sei ; запрещаем прерывания
ldx # $FF ; чтобы инициализировать стек надо записать $FF в X
txs ; и передать его в регистр вершины стека командой
; Transfer X to S (txs)
sta MMC3_IRQ_OFF ; Выключим IRQ маппера
; Теперь можно пользоваться стеком, например вызывать процедуры
jsr warm_up ; вызовем процедуру "разогрева" (см. neslib.s)
store MMC3_MIRROR, # MMC3_MIRROR_V ; Выставим вертикальное зеркалирование
store MMC3_RAM_PROTECT, # 0 ; Отключим RAM (если бы она даже была)
store_addr arg0w, palettes ; параметр arg0w = адрес наборов палитр
jsr fill_palettes ; вызовем процедуру копирования палитр в PPU
fill_ppu_addr PPU_SCR0 ; настроимся в начало первой видеостраницы
; и зальём в неё паттерн тайлов выводящий сформированный нами в тайлсете
; битмап с произвольным изображением
jsr fill_32_lines
; заливка 32 строк будет заливать 2 лишних строки (всего из 30, а не 32)
; и потому просто затрёт область атрибутов экранной области SCR0
; поэтому нам надо перенастроить PPU_ADDR чтобы выставить правильные палитры:
fill_ppu_addr PPU_SCR0_ATTRS ; настроимся на атрибуты первой видеостраницы
lda # 0 ; выберем в A нулевую палитру
jsr fill_attribs ; и зальём её область атрибутов SCR0
; Спрайты должны быть включены даже если они не нужны, иначе счётчик
; сканлайнов в MMC3 не будет работать, поэтому отключим все спрайты
; выводом их за границу отрисовки по Y
ldx # 0 ; В X разместим указатель на текущий спрайт
lda # $FF ; В A координата $FF по Y
sz_loop:
sta SPR_TBL, x ; Сохраним $FF в координату Y текущего спрайта
inx
inx
inx
inx ; И перейдём к следующему
bne sz_loop ; Если X не 0, то идём на следующую итерацию
; **********************************************
; * Стартуем видеочип и запускаем все процессы *
; **********************************************
; Включим генерацию прерываний по VBlank и источником тайлов для спрайтов
; сделаем второй банк видеоданных
store PPU_CTRL, # PPU_VBLANK_NMI | PPU_SPR_TBL_1000
; Включим отображение спрайтов и то что они отображаются в левых 8 столбцах пикселей
store PPU_MASK, # PPU_SHOW_BGR | PPU_SHOW_LEFT_BGR | PPU_SHOW_SPR | PPU_SHOW_LEFT_SPR
cli ; Разрешаем прерывания
Здесь мы выставляем всё как и договаривались. Главное включить и спрайты и фон, развести из по разным банках данных для тайлов ($0XXX для фона и $0XXX для спрайтов) и вывести спрайты за пределы экрана чтобы они были не видны ну и вызвать процедуру заполнения экранной области нужными индексами тайлов и индексов палитр.
Обязательно надо выполнить инструкцию cli, т.к. прерывания IRQ маскируемые, т.е. их обработку может сам себе запретить процессор исполнив инструкцию sei — тогда он просто не будет на них реагировать. А ведь эта инструкция — первая что исполняет наша программа при старте. В отличие от прерывания NMI от которого процессор может «отделаться» только записью во внешние порты тех устройств что их могут генерировать и рассчитаны на возможность запрета тут сам процессор может игнорировать прерывание если выставит этот бит запрета обработки прерывания IRQ в регистре флагов инструкцией sei. Поэтому надо явно разрешить эту обработку инструкцией cli.
Последняя часть кода — игровой цикл:
main.s — последняя часть, игровой цикл
; ***************************
; * Основной цикл программы *
; ***************************
main_loop:
jsr wait_nmi ; ждём наступления VBlank
; Чтобы обновить таблицу спрайтов в видеочипе надо записать в OAM_ADDR ноль
store OAM_ADDR, # 0
; И активировать DMA записью верхнего байта адреса страницы с описаниями
store OAM_DMA, # >SPR_TBL
; В начале кадра выставим в половинках банков графики CHR0
; банки с номерами 8..9..10..11 (т.к. мы работаем половинками,
; а не четвертинками, то нечеты не указываются явно)
mmc3_set_bank_page # MMC3_CHR_H0, # 8
mmc3_set_bank_page # MMC3_CHR_H1, # 10
; Следующий банк видеоданных на который надо переключится в
; прерывании - 12 и мы будем хранить его в next_page:
store next_page, # 12
; Счётчик сканлайнов маппера надо выставить в 63, а не 64, т.к. процедура
; прерывания будет сама пропускать один сканлайн:
store MMC3_IRQ_COUNTER, # 63
; Выставим флаг того, что на следующем сканлайне надо перезагрузить
; счётчик сканлайнов значением из IRQ_COUNTER
sta MMC3_IRQ_RELOAD
; Включим генерацию прерывания IRQ маппером
sta MMC3_IRQ_ON
store PPU_SCROLL, # 0 ; Перед началом кадра выставим скроллинг
store PPU_SCROLL, # 0 ; в (0, 0) чтобы панель рисовалась фиксированно
; ********************************************************
; * После работы с VRAM можно заняться другими вещами... *
; ********************************************************
jsr update_keys ; Обновим состояние кнопок опросив геймпады
jmp main_loop ; И уходим ждать нового VBlank в бесконечном цикле
.endproc
И вот тут всё очень кратко — сперва заполняем таблицу спрайтов так чтобы все они были не видны выводом координаты Y за границы экрана, потом маппим текущие данные тайлов для фона на страницы 8-11 (опять таки помним, что мы тут используем маппинг таблицы тайлов в режиме «половинок», поэтому указываются только чётные два адреса — нечеты выставятся самим чипом). А в переменную next_page сохраним число 12 — это номер начала следующих банков куда надо будет выставить маппинг видеостраниц по прерыванию от счётчика сканлайнов MMC3.
Далее важная вещь — мы еще находимся в VBlank и добрались до места где счётчик сканлайнов уже нельзя будет «неправильно возбудить» записью в видеопамять. Здесь мы записью (любого значения) в порт MMC3_IRQ_RELOAD выставляем флаг того что при следующем срабатывании счётчика (а это будет первый «невидимый» сканлайн кадра который последует за VBlank) его надо перезагрузить значением 63 и сперва записываем это значение в порт MMC3_IRQ_COUNTER. Так мы гарантируем, что счётчик чем бы он сейчас ни был станет в начале кадра чем ему нужно быть.
Ну и как бы всё! Записываем любое значение в порт MMC3_IRQ_ON чтобы включить генерацию прерываний и наслаждаемся результатом и здесь не будет видео, т.к. здесь нет ничего динамичного — просто картинка, но вот скриншот:
Итак, перехват прерывания по сканлайну в MMC3 намного более гибкая штука чем «ушибление спрайта», но в следующем уроке я еще покажу как правильно скроллить с её помощью игровое поле во всех двух игровых измерениях сохраняя неподвижную «панель состояния» в сверху или снизу экрана и это будет последний урок в этом цикле.
В первую часть (оглавление)...
0 комментариев