У меня нет доступа к официальной документации Picture Processing Unit (PPU — графический чип) консоли NES, поэтому мои заявления о «неопределённом поведении» скорее ближе к догадкам. Спецификацию работы графического оборудования я взял из NesDev Wiki. PPU управляется записью в регистры с отображением в память. Если использовать эти регистры так, как это было (похоже) задумано проектировщиками, то добиться этого эффекта было бы невозможно:
При скроллинге экрана по вертикали весь экран должен скроллиться разом. В предыдущем GIF показан пример частичного вертикального скроллинга. Часть экрана остаётся стационарной (элементы интерфейса), а другая часть (игровая область) прокручивается по вертикали. Частичный вертикальный скроллинг невозможно реализовать при «стандартной» работе с PPU.
В отличие от него, частичный горизонтальный скроллинг полностью определён и возможен.
Запись в отдельный регистр PPU в момент отрисовки кадра может привести к графическим артефактам. The Legend of Zelda намеренно вызывает артефакт, который проявляется как частичный вертикальный скроллинг. В этом посте я немного расскажу о графическом оборудовании NES и объясню, как работает трюк с вертикальным скроллингом.
Виды графики
У консоли NES есть два вида графики:
- Спрайты — тайлы, которые можно размещать в произвольных местах экрана и перемещать независимо друг от друга.
- Фон — сетка тайлов, которые можно плавно прокручивать как единое изображение.
Чтобы продемонстрировать разницу между ними, я покажу сцену, составленную из спрайтов и фона:
А вот та же сцена, в которой видны только спрайты:
А вот сцена, в которой виден только фон:
Скроллинг
Процессор изображений (NES Picture Processor) поддерживает скроллинг фоновых изображений. В видеопамяти графика фона хранится в виде двухмерной сетки тайлов, покрывающей область, в два раза превышающую ширину и высоту экрана.
На экране отображается «окно» в этой сетке размером с экран, и положением этого окна можно точно управлять. При постепенном перемещении видимого окна по сетке создаётся эффект плавного скроллинга.
Выводимый NES видеосигнал имеет размер 256x240 пикселей. Сетка тайлов внутри памти представлена как область пикселей размером 512x480 и разбита на четыре прямоугольника размером с экран, которые называются «таблицами имён» (name tables). Игры могут конфигурировать Picture Processing Unit (PPU), указывая положение видимого окна выбором координаты пикселя в сетке таблиц имён.
При выборе координаты (0, 0) на экране отобразится вся верхняя левая таблица имён:
Переместившись в (125, 181), мы увидим немного от каждой таблицы имён:
Видимое окно сворачивается к дальней части сетки тайлов в памяти. Переместившись в (342, 290), мы поместим верхний левый угол видимого экрана внутрь нижней правой таблицы имён, и благодаря сворачиванию будут видны части каждой из таблиц имён:
Недостаточно памяти!
Каждая таблица имён имеет размер 1 КБ, но NES выделяет на эти таблицы всего 2 КБ своей видеопамяти, поэтому одновременно в памяти могут поместиться только две таблицы имён.
Как в ней может быть четыре таблицы имён?
Отзеркаливание таблиц имён
Видеопамять соединена с PPU таким образом, что когда PPU рендерит тайл одной из четырёх кажущихся таблиц имён, на самом деле выбирается одна из двух реальных таблиц, и считывание происходит оттуда. По сути это означает, что четыре видимые таблицы имён на самом деле составлены из двух одинаковых пар таблиц.
На этом изображении показан снимок содержимого всех четырёх таблиц. Верхняя левая и верхняя правая одинаковы, как и обе нижние.
Почему бы тогда просто не хранить две таблицы имён?
К счастью, точная привязка между кажущейся и реальной таблицами может конфигурироваться во время выполнения. Если игра хочет выполнить горизонтальный скроллинг, то она настраивает графическое оборудование так, чтобы отличались верхняя левая и верхняя правая таблицы, и их можно было прокручивать без заметного дублирования. В такой конфигурации верхняя левая и нижняя левая таблицы будут ссылаться на одну реальную таблицу имён; аналогично и для двух правых таблиц. Такая конфигурация называется «вертикальным отзеркаливанием» (Vertical Mirroring).
Существует также ещё одна возможная конфигурация — «горизонтальное отзеркаливание» (Horizontal Mirroring), которую игры используют для вертикального скроллинга.
Обычно игры не скроллятся по диагонали, потому что это создаёт артефакты по краям экрана из-за отзеркаливания таблиц имён.
Картриджи
В картридже каждой игры есть «железо», позволяющее конфигурировать отзеркаливание таблиц.
В некоторых играх вообще не нужно переключать отзеркаливание, поэтому в их картриджах жёстко прописано горизонтальное или вертикальное отзеркаливание. Другие игры динамически переключаются между этими двумя режимами, поэтому отзеркаливание в их картриджах настраивается программно. The Legend of Zelda относится ко второй категории. Наконец, в картриджах некоторых по-настоящему сложных игр есть дополнительная видеопамять, то есть отзеркаливание им вообще не нужно: они могут выполнять одновременный скроллинг по вертикали и горизонтали без видимых артефактов дублирования.
Реальный пример
Пример вертикального скроллинга, который отображается на экране.
Здесь показана запись таблиц имён с горизонтальным отзеркаливанием. Текущее видимое окно подсвечено.
Помните, что в самом вертикальном скроллинге нет ничего необычного — необычность заключается в вертикальном скроллинге с разделением экрана.
Разделение экрана
Каждый кадр видеосигнала, создаваемого NES, отрисовывается сверху вниз, по одной строке пикселей за раз. В каждой строке пиксели отрисовываться по одному за раз, слева направо. На полпути при отрисовке кадра игра может перенастроить PPU, что влияет на отображение пикселей, которые пока не отрисованы. Одним из самых распространённых изменений посередине кадра является обновление позиции горизонтального скроллинга.
При горизонтальном скроллинге между комнатами The Legend of Zelda всегда начинает с позиции скроллинга (0, 0) и рендерит элементы интерфейса в верхней части экрана. После отрисовки на экране последней строки пикселей интерфейса горизонтальный скроллинг изменяется на величину, которая с каждым кадром увеличивается, благодаря чему камера перемещается плавно.
Анимация отображения таблиц имён показывает, как игра перед началом скроллинга переключается с горизонтального на вертикальное отзеркаливание, а затем снова на горизонтальное после завершения перехода. Кроме того, пока скроллинг продолжается, верхняя левая (и нижняя левая) таблицы имён обновляются, в них записывается копия комнаты, в которую входит игрок. После завершения скроллинга игра перестаёт разделять экран и снова целиком рендерится из верхней левой таблицы.
Измерение степени отрисовки
Чтобы разделять экран в нужной позиции, игре нужно каким-то образом узнавать, какая часть текущего кадра была отрисована. Строки пикселей рендерятся с известной частотой, поэтому номер отрисовываемой строки пикселей можно определить, подсчитав количество тактов процессора, прошедших с начала кадра.
Существует и другая, более точная техника под названием Sprite Zero Hit.
NES одновременно может отрисовывать до 64 спрайтов. Первый спрайт в видеопамяти называют Sprite Zero (нулевым спрайтом). В каждом кадре, как только непрозрачный пиксель нулевого спрайта накладывается на непрозрачный пиксель фона, происходит событие Sprite Zero Hit. Оно задаёт бит в одном из регистров PPU с отображением в память, который может проверяться процессором.
Чтобы использовать Sprite Zero Hit для разделения экрана, игры располагают нулевой спрайт в вертикальной позиции рядом с границей разделения, и во время рендеринга постоянно проверяют, произошло ли событие Sprite Zero Hit. Если да, то игра переключается с горизонтального скроллинга для реализации разделения.
Ниже показан горизонтальный переход между комнатами с фоном и без него.
Коричневый круг, появляющийся в начале перехода и исчезающий в его конце — это нулевой спрайт. Внимательнее рассмотрим интерфейс с фоном и без него:
Нулевой спрайт — это обесцвеченный спрайт бомбы, идеально совпадающий по расположению с обычным спрайтом бомбы из интерфейса игры. Нулевой спрайт настроен так, чтобы отображаться под фоном, но поскольку чёрные пиксели интерфейса считаются прозрачными, бомба нулевого спрайта была бы видимой, если бы не её стратегически не спрятали за бомбой из интерфейса.
Заметьте, что Sprite Zero Hit происходит за несколько строк пикселей до нижней строки интерфейса. Он происходит на верхнем пикселе запала бомбы, который находится в 16 пикселях от низа интерфейса. Когда происходит Sprite Zero Hit, игра начинает считать циклы процессора, и после завершения нужного количества циклов устанавливает горизонтальный скроллинг.
Гашение обратного хода луча
Бo?льшую часть времени PPU консоли отрисовывает пиксели на экран. Существует короткий период простоя между кадрами, во время которого отрисовка не выполняется. Это явление называется гашением обратного хода луча (Vertical Blank, или vblank). Некоторые виды изменений конфигурации PPU можно выполнять только во время vblank.
Регистр скроллинга
Игры изменяют позицию скроллинга, осуществляя запись в регистр PPU под названием
PPUSCROLL
, который отображается на адрес памяти 0x2005
. Первая операция записи в PPUSCROLL
задаёт компонент X позиции скроллинга, а вторая операция задаёт компонент Y. Аналогичным образом попеременная запись выполняется и дальше.Ниже показаны все ненулевые операции записи в
PPUSCROLL
во время этого воспроизведения (в замедленном действии) 16 кадров экрана с сюжетом игры. Компонент Y позиции скроллинга увеличивается раз в два кадра. Все операции записи в PPUSCROLL
в этом примере выполняются во время vblank, что заставляет прокручиваться вместе с этим и весь фон.Скроллинг разделения экрана
Операции записи в
PPUSCROLL
во время vblank вступают в силу в начале кадра, отрисовываемого непосредственно после vblank. Если позиция скроллинга изменяется во время отрисовки кадра (т.е. не во время vblank), то это изменение вступает в силу, когда отрисовка доходит до следующей строки пикселей. Частичный горизонтальный скроллинг реализуется записью в PPUSCROLL
во время отрисовки устройством PPU последней строки пикселей перед скроллингом.При обновлении позиции скроллинга в середине кадра применяется только компонент X позиции скроллинга. То есть компонент Y позиции скроллинга отбрасывается. Таким образом, если игра хочет разделить экран и меняет позицию скроллинга части кадра, то может выполнять скроллинг только по горизонтали.
И тем не менее:
Хотите верьте, хотите нет, но во время этого перехода значение регистра
PPUSCROLL
не менялось.Можно заметить под интерфейсом графический артефакт высотой в один пиксель. Это баг моего эмулятора, вызванный отсутствием синхронизации тактовых циклов процессора с попиксельным рендерингом.
Вмешательство в другие регистры
Второй регистр под названием
PPUADDR
, отображаемый на адрес памяти 0x2006
,, используется для задания текущего адреса видеопамяти. Когда игра, например, хочет изменить один из тайлов в таблице имён, она сначала выполняет запись адреса видеопамяти тайла в PPUADDR
, а затем записывает новое значение тайла в PPUDATA
— это третий регистр, отображаемый на адрес 0x2007
.Запись в
PPUADDR
не во время vblank (т.е. при отрисовке кадра) может вызывать графические артефакты. Так получается потому, что цепь PPU, на которую влияет запись в PPUADDR
, также непосредственно управляется устройством PPU в процессе получения тайлов из видеопамяти для их отрисовки. Так как процесс отрисовки на экран выполняется сверху вниз, и слева направо в пределах строки, то PPU по сути присваивает PPUADDR
значение адреса текущего отрисовываемого тайла. Когда отрисовка переходит от одного тайла к другому, PPUADDR
изменяется инкрементом текущего значения.Таким образом, запись в
PPUADDR
в середине кадра может изменить тайлы, получаемые PPU из памяти на время текущего кадра.Давайте выведем операции записи в
PPUADDR
во время вертикального перехода. Поскольку во время перехода таблица имён тоже обновляется, вывод всех операций записи в PPUADDR
будет слишком обширным. При горизонтальном переходе скроллинг задаётся во время отрисовки строки пикселей 63, поэтому рассмотрим операции записи в PPUADDR
только во время этой строки.Чётко видна закономерность. Через каждые два кадра адрес, записываемый в строке пикселей 63, уменьшается на 32 (0x20). Но как это приводит к обновлению фактической позиции скроллинга?
Настоящий регистр скроллинга
Внутри PPU есть 15-битный регистр, не отображаемый в память ЦП. Он используется и как текущий адрес для доступа к видеопамяти, и как конфигурация скроллинга фона.
При работе с этим значением как с адресом бит 14 игнорируется, а биты 0-13 обрабатываются как адрес в видеопамяти.
При работе с этим значением как с конфигурацией скроллинга, разные его части имеют различное значение:
Выбор таблицы имён — это значение от 0 до 3, определяющее текущую таблицу имён, из которой производится отрисовка.
Грубый скроллинг по X и Грубый скроллинг по Y определяют координату тайла внутри выбранной таблицы имён. Это текущий отрисовываемый тайл.
Точный скроллинг по Y содержит значение от 0 до 7, определяющее текущее вертикальное смещение строки пикселей внутри текущего тайла. Тайлы — это квадраты со стороной 8 пикселей.
Точный скроллинг по X в этом регистре отсутствует. Существует отдельный регистр, содержащий только горизонтальное смещение текущего пикселя, но он не важен для объяснения того, как в The Legend of Zelda выполняется вертикальный скроллинг.
Что происходит с этим регистром, когда игра выполняет запись в
PPUADDR
? Вот первые три операции записи из показанного выше демо.Разбив записи по адресу на компоненты скроллинга, можно чётко понять, что здесь происходит. Через каждые два кадра уменьшается значение Грубого скроллинга по Y, что приводит к вертикальному скроллингу на один тайл или 8 пикселей.
На протяжении каждого кадра изначальное смещение скроллинга равно 0,0, после чего на строке пикселей 63 выполняется запись по адресу. Это значит, что первые 63 строки пикселей отрисовываются с вершины выбранной таблицы имён, содержащей фон интерфейса. Однако 64-я строка пикселей и далее отрисовываются с вертикальным скроллингом, применённым из этого адреса. Так как вертикальный скроллинг уменьшается через каждые два кадра, это даёт ощущение вертикального скроллинга части экрана.
Скроллим вниз, чтобы скроллить вверх
The Legend of Zelda не может скрыть этот трюк от игроков полностью. Он создаёт видимый артефакт на вертикальных переходах экрана, которые заметен, если присмотреться. При переходе между комнатами первый кадр анимации скроллинга скроллится вниз. Вот анимация в очень замедленном действии.
В режиме таблиц имён видно, что происходит на самом деле. Хотя игрокам может казаться, что видимая область плавно скроллится вверх, переход скроллинга начинается с перемещения видимой области из верхней левой таблицы имён в нижнюю левую таблицу, которая содержит копию фона комнаты. Это необходимо, потому что интерфейс в верхней части экрана тоже является частью таблицы имён, и если бы видимая область скроллилась вверх из исходной позиции, то она бы проходила по интерфейсу.
Вертикальный скроллинг реализован записью в регистр
PPUADDR
в середине кадра. Самым первым записывается значение 0x2800
. Два кадра спустя записывается 0x23A0
, а затем значение начинает уменьшаться на 32 каждый второй кадр.Запись значения
0x2800
в регистр PPUADDR
присваивает Выбору таблицы имён значение 2, что приводит к рендерингу нижней левой таблицы имён. Поскольку оба значения скроллинга равны 0, он начнётся с верхнего левого тайла этой таблицы имён. Однако Точный скроллинг по Y равен 2, поэтому присутствует двухпиксельное вертикальное смещение от верха нижней левой таблицы имён. Именно поэтому в самом первом кадре перехода мы видим внизу экрана чёрную полосу высотой в 2 пикселя. Изначальное значение скроллинга для анимации перехода смещено на 2 пикселя вниз, чтобы переход был бесшовным.Два кадра спустя в
PPUADDR
записывается значение 0x23A0
. Это переносит нас обратно к верхней левой таблице имён, и мы рендерим с 29-й строки тайлов, то есть самой нижней. В Точном скроллинге по Y по-прежнему содержится 2.Почему необходимо присваивать Точному скроллингу по Y значение 2? Почему бы игре просто не записать
0x0800
и 0x03A0
, чтобы не страдать от двухпиксельного смещения?Четыре таблицы имён занимают область 4 КБ в адресном пространстве PPU, от
0x2000
до 0x2FFF
. Каждый тайл в таблице занимает один байт видеопамяти (на самом деле они являются всего лишь индексами в другой таблице), и порядок тайлов и таблиц имён в видеопамяти таков, что Выбор таблицы имён, Грубый скроллинг по Y и Грубый скроллинг по X составляют смещение тайла внутри области памяти с таблицами имён. То есть взяв младшие 12 битов внутреннего регистра PPU и прибавив их к 0x2000
, можно найти адрес тайла в видеопамяти. И это не совпадение! Именно так и должен обрабатываться регистр: и как регистр адреса, и как регистр скроллинга.Но здесь есть один изъян.
При обработке в качестве регистра адреса биты 12 и 13 считаются частью адреса. Во время рендеринга PPU постоянно переписывает регистр адресом текущего отрисовываемого тайла. Так как тайлы расположены в таблицах имён, а таблицы расположены в области памяти с
0x2000
до 0x2FFF
, PPU присваивает регистру значения из этого интервала.Когда игра выполняет запись в
PPUADDR
в середине кадра, если она не запишет адрес тайла в таблице имён, то PPU попытается выполнить считывание откуда-то ещё в видеопамяти. Любые байты, которые ему доведётся считать, будут восприниматься как тайлы, что с большой вероятностью приведёт к нежелательным результатам. Поэтому все значения, записываемые посередине кадра в PPUADDR
, должны находиться в интервале от 0x2000
до 0x2FFF
. Взяв каждое число в этом интервале и учитывая его компоненты скроллинга, значение Точного скроллинга по Y всегда должно быть равно 2.Это ограничение означает, что мы не можем изменять Точный скроллинг по Y посередине кадра, то есть при использовании этого трюка для реализации вертикального скроллинга разделения экрана мы ограничены скроллингом по 8 пикселей за раз и всегда двухпиксельным вертикальным смещением от границы тайла. The Legend of Zelda выполняет перемещение по 4 пикселя за кадр при горизонтальном скроллинге, но по 8 пикселей через кадр при вертикальном скроллинге, и теперь мы знаем, почему.
Артефакт также заметен при скроллинге между комнатами вниз, но в этом случае он возникает в конце анимации.
Дополнительное чтение
- NesDev Wiki — бесценный ресурс для изучения аппаратной части NES. В частности, к теме этого поста относятся страницы о скроллинге PPU
и регистрах PPU. - Мой по-прежнему очень недоделанный эмулятор NES выложен здесь.
Примечания
Пока я не узнал о внутреннем регистре PPU, мой эмулятор показывал при вертикальных переходах экрана The Legend of Zelda эффект стирания.
Спрайт Линка переносился вниз экрана, как и должно быть, но фон не скроллился. Стирание вызывалось тем, что игра постепенно обновляла таблицу имён, чтобы она содержала графику новой комнаты, но не обновляла скроллинг, чтобы хранить обновления за пределами экрана.
Комментарии (8)
screwer
19.07.2019 01:44Как-то слишком сложно. На NES была целая куча игр с неподвижной областью и скроллингом. Навскидку PowerBlade 2. Там вообще PPU использован на полную — когда часть экрана скроллится вертикально, и при этом он весь скроллится горизонтально (уррвень с бензопилами).
Насколько я в курсе, реализуется это довольно просто. Есть аппаратный счетчик, который декрементится при отрисовке очередной скан линии. При его обнулении возникает nmi. Вот и все. В обработчике меняем координаты скроллинга. И никакие такты считать не надо.
oldschoolgeek
19.07.2019 15:38Под закат NES, когда уже научились вовсю дёшево клепать заказные СБИС для картриджей, так и поступали. Но, до этого, делать счётчик на «рассыпухе» и массово штамповать в каждый картридж с игрой было недешёвым удовольствием — поэтому, в ранних играх приходилось порой и такты считать, чтобы не удорожать производство картриджа.
rock3tup
19.07.2019 09:50Читая статью вспомнил про историю с игрой Bio Force Ape для NES. В свое время она так и не была выпущена, но в 2010г. картридж с прототипом игры был выставлен на интернет аукционе.
Вскоре игру выкупили, сделали образ и выложили в интернет. Как оказалось в игре была очень плавная анимация спрайтов. Т.к. я не являюсь технически подкованным в этой теме, но мне было бы интересно услышать комментарии знающих людей:
-являются ли технические решения разработчика игры Bio Force Ape чем то выдающимся и оригинальным или нет?
oldschoolgeek
Спасибо, очень познавательная статья! Вместе с тем, насколько я читал, перепрограммирование PPU «на лету» не было чем-то сверхсекретным и активно использовалось при разработке игр на разных платформах того времени. Говоря конкретно о NES, некоторые картриджи даже содержали дополнительную электронику, генерировавшую прерывания по обратному ходу луча — без чего было сложно обойтись, если перепрограммирование PPU происходило более одного раза за кадр (прозрачных спрайтов не напасёшься )
adictive_max
[wiki] [геймлей] [обзор]
oldschoolgeek
:-O :-O :-O если бы такое вышло на NES (Dendy) в начале 90х, это была бы реально культовая игра!
Диагональный скроллинг, правда, требовал не сколько умопомрачительных трюков с PPU, сколько дополнительной видеопамяти, достаточной, чтобы уместить все четыре виртуальных экрана.
guryanov
В battle toads тоже диагональный скроллинг https://www.youtube.com/watch?v=f3H-Yk1V3KA&t=81s