Программирование для 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

Проект Example06

Создадим новый проект на базе предыдущего (Example05). Так все файлы в папке src будут такими же кроме main.s. nes.ini тоже будет совершенно таким же.
Однако в Example05 мы наводнили 64Кб графических данных клонами двух картинок, здесь же мы создадим 16 уникальных графических файла:
  1. tiles00A.png
  2. tiles00B.png
  3. tiles01A.png
  4. tiles01B.png
  5. tiles02A.png
  6. tiles02B.png
  7. tiles03A.png
  8. tiles03B.png
  9. tiles04A.png
  10. tiles04B.png
  11. tiles05A.png
  12. tiles05B.png
  13. tiles06A.png
  14. tiles06B.png
  15. tiles07A.png
  16. tiles07B.png
и назначим их в 4-кбайтные банки в интерфейсе Nesicide по порядку.
В первые два банка мы назначим то же самое что в предыдущем уроке было в файлах 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 комментариев

Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.