Подводные камни скроллинга на Famicom/NES/Денди

Как игровая консоль денди конечно же должна была поддерживать аппаратный скроллинг и делала это.
Но с этим связано несколько подводных камней которые мне показались достаточно забавными чтобы написать о них тут отдельно.

Для начала немного ликбеза для тех кто не в курсе как устроена видеопамять в денди или подзабыл:

Видеочип консоли обладал собственным изолированным от основного процессора пространством памяти в котором нас сейчас интересуют две вещи:
а) первые 8Кб видеопамяти (от $0000 до $1FFF) содержали 512 изображений тайлов размером 8x8 пикселей цветностью 4 цвета (2 бита) на пиксель. Одна половина этих тайлов обычно служила источником изображения для плиток фона, а другая — для подвижных спрайтов. В играх первого поколения (Galaxian, Pacman, Bomberman и т.п.) эти 8Кб обычно маппились прямо на микросхему ПЗУ в картридже (это, например, Pacman):


Здесь она справа.
б) а с адреса $2000 располагались в VRAM четыре экранных области плиток фона (nametables) каждой из которых хватало чтобы полностью замостить один экран изображения неким фоном.
Это 256x240 пикселей изображения или 32x30 плиток тайлов, но надо учитывать, что не все пиксели по вертикали попадали на экран телевизора (особенно в системе телевидения NTSC).
Геометрически эти четыре области/страницы были пристыкованы друг к другу следующим образом:

     (0,0)     (256,0)     (511,0)
       +-----------+-----------+
       |           |           |
       |           |           |
       |   $2000   |   $2400   |
       |           |           |
       |           |           |
(0,240)+-----------+-----------+(511,240)
       |           |           |
       |           |           |
       |   $2800   |   $2C00   |
       |           |           |
       |           |           |
       +-----------+-----------+
     (0,479)   (256,479)   (511,479)

Если задействовать аппаратный скроллинг, то камера как бы летает над этим полем тайлов бесшовно «прокручиваясь» через края, что позволяет реализовать бесконечный скроллинг. Есть только большое «но» — конкретно в денди для экономии нижние две таблицы были выпилены и по сути в силу игнорирования адресной линии просто маппились на верхние (зеркалировались), при этом перестановкой линий адреса с картриджа можно было управлять между вертикальным и горизонтальным зеркалированием — для вертикальных и горизонтальных скроллеров удобны были разные режимы.
Сами эти страницы были устроены достаточно просто: сперва в них располагалась область из 32x30 (960) байт где каждый байт выбирал одно из изображений тайлов вышеописанных тайловых данных, а последние 64 байта страницы содержали биты выбора одной из четырёх палитр для каждого блока 2x2 тайла страницы. В один байт было запаковано четыре таких селектора палитр, поэтому каждый байт хранил цветовые атрибуты блока 4x4 тайла или 32x32 пикселя. Поэтому в том же супермарио размер макроблоков был как раз 4x4 тайла — это просто удобно.
Итак мы подходим к главному — эти два килобайта содержимого видеостраниц фона в денди должен был обновлять процессор.
Но прямого доступа к ним он не имел, поэтому всё взаимодействие с ними осуществлялось через порты ввода-вывода, а конкретно PPUADDR (ячейка памяти $2006 в адресном пространстве процессора) и PPUDATA ($2007).
Процессор записывает в PPUADDR подряд два байта адреса в VRAM (причём сперва старший, а потом младший), а потом записывает в PPUDATA байты которые попадут в VRAM или читает из PPUDATA байты из неё. Правда чтение осуществляется с задержкой в одно чтение из-за промежуточного регистра-буфера, но запись идёт сразу. Для скорости адрес в PPUADDR автоматически увеличивается на 1 или 32 в зависимости от бита 2 (нумерация с нуля) в управляющем регистре PPUCTRL ($2000).
Так и только так обновляются плитки фона в денди и есть одно важное ограничение: делать это можно только когда видеочип не рисует картинку, а отдыхает в коротком промежутке времени между построениями кадра — так называемый период VBlank.

Вернёмся к скроллингу — штатным способом скроллить изображение в денди является порт ввода-вывода PPUSCROLL ($2005) и два нижних бита в уже упомянутом управляющем регистре PPUCTRL ($2000). В некоторых руководствах часто пишут, что нижние 2 бита PPUCTRL как бы выбирают какая из четырёх видеостраниц с рисунка выше является «базовой», от которой скроллинг отталкивается, а две подряд идущих записи в PPUSCROLL выставляют сперва значение горизонтального скроллинга (0-255) в этой области, а потом вертикального (0-239). Под значением скроллинга понимается координата пикселя который будет левым-верхних на экране.
Но мне нравится другая трактовка — что в двух нижних битах PPUCTRL как бы находятся самые значащие верхние биты «координат прокрутки» от 0 до 512 в едином четверном пространстве видеопамяти (но для вертикальных надо иметь ввиду пропуск в середине где заканчиваются 30 вертикальных строк тайлов первых видеостраниц).
Всё кажется просто и логично? Но сейчас пойдут мелочи в которых кроется дьявол.

Первое что должно нас насторожить — это то, что запись в PPUSCROLL разрушает содержимое регистра PPUADDR и по официальной документации выставление скроллинга должно быть последним действием после всех записей в VRAM.
Второе — это когда мы захотим поменять значение скроллинга прямо в середине построения кадра — это может быть полезно для реализации сплит-скрина или HBlank-отсечения, о котором уже есть статья. Тут выясняется, что запись в PPUSCROLL даже во время HBlank может изменить только величину горизонтального скроллинга, но вертикальный может сменится только в конце VBlank когда видеочип приступает к отрисовке нового кадра.
Например так можно сделать скроллинг в игре Bomberman где содержимое видеостраниц в VRAM выглядит вот так:

А на экране телевизора после скроллинга и наложения спрайтов уже вот так:


Здесь как раз горизонтальный разрыв в принципе может быть сделан через запись в PPUSCROLL. Но в огромном количестве игр эксплуатирующих HBlank-отсечение нужно еще уметь менять вертикальную прокрутку и вот чтобы понять как это сделать и что тут вообще происходит нужно опустится в глубины того как прокрутка реализована в кишках денди.

А в кишках денди можно выделить четыре интересующих нас регистра (согласно статье по предыдущей ссылке):
v — 15 бит текущего адреса VRAM (PPUADDR)
t — 15 бит временного адреса VRAM
w — 1 бит признака второй записи
x — 3 бита попиксельного скроллинга по горизонтали внутри тайла

Шина адреса VRAM 14-битная, но v и t имеют размер в 15 бит по причинам описанным ниже, в любом случае эффективны из них только 14 нижних бит. Когда я буду говорить про «верхний байт» регистров v или t я на самом деле буду иметь 7 верхних бит.
При записи байта в PPUADDR сперва анализируется бит w — если он равен 0 (первая запись), то байт записывается в верхний байт t с занулением верхнего бита (т.е. из записанного байта попадают в регистр нижние 6 бит) и бит w выставляется в 1. А если при записи w=1, то запись производится в нижний байт t и сразу после этого t копируется в v.
Таким образом запись в PPUADDR довольно прямолинейно выставляет адрес в t и v с занулением верхнего (неэффективного) бита №15, которое, как говорится в статье, непонятно зачем сделано и только вставит нам палку в колёса ниже.

Запись в PPUSCROLL под капотом пишет в те же самые регистры v и t, но делает это более изощрённо и так же затрагивает регистр x.
Но чтобы перейти к объяснению надо еще вспомнить про нижние биты в управляющем регистре PPUCTRL которые управляли в какой из четырёх видеостраниц располагается базис текущего экрана.
Когда мы пишем в PPUCTRL кроме всего остального нижние два его бита записываются в биты t следующим образом:

t: ---XY-- --------  ; биты обозначенные как - не меняются

При первой записи (опять таки она определяется по общему флагу w и я больше про него упоминать не буду) байта (прокрутки по X) в PPUSCROLL его биты (пронумеруем их как '76543210') попадают в t так:

t: ------- ---76543 ; т.е. биты с 7 по 3 попадают в нижние биты t
x: 210 ; и при этом нижние 3 бита откладываются в регистре x

Опять таки биты помеченные как — не меняются.
И вот при второй записи в PPUSCROLL байта прокрутки по Y его биты попадают в t уже так:

t: 210--76 543-----

И больше ничего не происходит — регистр v не вообще трогается. Такая замысловатая разброска бит на самом деле просто выставляет в t адрес того байта с которого надо начать рисовать плитки фона в VRAM и кроме того откладывает горизонтальные и вертикальные смещения внутри одного знакоместа/тайла в регистр x и в верхние 3 бита регистра t. Тут надо сразу заметить, что верхние 3 бита регистра v действительно будут использоваться как вертикальное смещение внутри тайла и при чтении из VRAM участвовать в адресе не будут.

Но за всем этим пока еще не видно собственно скроллинга. А происходит он вот как:
1. в конце VBlank перед тем как начать рисовать новый кадр PPU копирует биты ответственные за координату Y из t в v, а конкретно эти биты: XXXX-XX XXX----- (опять же минусы — значит не копируются)
2. по мере отрисовки сканлайна PPU читает из VRAM по адресу v инкрементируя адрес вдоль координаты x (с прокруткой через страницы и пропуском атрибутных зон)
3. когда отрисовка текущего сканлайна завершается PPU нужно восстановить в v изначальное положение по X и перейти к следующей строке.
для этого сперва из t копируется часть бит ответственных за координату X, а именно: ----X-- ---XXXXX
тут надо понимать, что существует один сканлайн выполняющий это до первой эффективной строки пикселей — в нём делается много других важных предустановок, но в т.ч. и эта и таким образом вместе с пунктом (1) в v оказывается полная копия t перед тем как изображение начинается рисоваться
4. переходя от сканлайна к сканлайну PPU увеличивает в v адресную часть ответственную за координату Y

Так вот что тут происходит — мы действительно не можем через PPUSCROLL поменять вертикальную прокрутку в середине кадра, т.к. её биты попадают из t в v только в конце VBlank перед тем как изображение начинает рисоваться. При этом в t как бы и образуется правильный адрес, но на v он может повлиять только битами ответственными за X.
При этом заметьте, что если мы попытаемся просто выставить сразу нужный адрес через запись напрямую в PPUADDR, то из-за того, что «глупая железка» зачем то обнуляет верхний бит адреса, то может оказаться поломанным пиксельное смещение по Y внутри знакоместа/тайла.
Какой то прям замкнутый круг?
Однако инженерная мысль обошла эти подводные камни мутных вод программирования 8-битной консоли с лёгкостью и изяществом достойным упоминания тут.
Достаточно сделать четыре записи в PPUSCROLL и PPUADDR в «неправильном» порядке — сперва одна запись в PPUADDR, потом две записи в PPUSCROLL и потом последняя опять в PPUADDR.
Почему это работает: во первых т.к. бит второй записи w общий на все операции, то записи и не обязаны быть попарными, но все действия будут легко предсказуемыми.

В результате сложилась следующая схема (всё взято из статьи):
1. в PPUADDR пишем номер страницы помноженный на 4 (т.е. одно из значений: 0, 4, 8 или 12)
2. в PPUSCROLL пишем байт прокрутки внутри страницы по Y
3. в PPUSCROLL пишем байт прокрутки внутри страницы по X
4. в PPUADDR пишем нижний байт адреса который вычисляется как ((Y & $F8) << 2) | (X >> 3)
«Размотать» при этом какие биты куда попадают кто захочет может уже самостоятельно, но главное что при такой последовательности и насильно и непонятно зачем зануляемый бит оказывается переписан правильным значением и регистр v получит в себя копию из t не только бит ответственных за координату X, но и Y.
В связи с таким положением в некоторых руководствах по Famicom/NES/Денди даже возникло мнение, что любой реализации скроллинга обязательно требуется шаманить с бубном вокруг регистра PPUADDR вместе с PPUSCROLL, но это мнение уже на сайте wiki.nesdev.com/w/index.php/Myths занесено в раздел мифов.

0 комментариев

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