Хабрахабр

[Перевод] Переходы между экранами в Legend of Zelda используют недокументированные возможности NES

Для эффекта вертикального скроллинга в первой части «The Legend of Zelda» используются манипуляции графическим «железом» NES, скорее всего не предусмотренные разработчиками консоли.

У меня нет доступа к официальной документации Picture Processing Unit (PPU — графический чип) консоли NES, поэтому мои заявления о «неопределённом поведении» скорее ближе к догадкам. Спецификацию работы графического оборудования я взял из NesDev Wiki. PPU управляется записью в регистры с отображением в память. Если использовать эти регистры так, как это было (похоже) задумано проектировщиками, то добиться этого эффекта было бы невозможно:

При скроллинге экрана по вертикали весь экран должен скроллиться разом. В предыдущем GIF показан пример частичного вертикального скроллинга. Часть экрана остаётся стационарной (элементы интерфейса), а другая часть (игровая область) прокручивается по вертикали. Частичный вертикальный скроллинг невозможно реализовать при «стандартной» работе с PPU.

В отличие от него, частичный горизонтальный скроллинг полностью определён и возможен.

Запись в отдельный регистр PPU в момент отрисовки кадра может привести к графическим артефактам. The Legend of Zelda намеренно вызывает артефакт, который проявляется как частичный вертикальный скроллинг. В этом посте я немного расскажу о графическом оборудовании NES и объясню, как работает трюк с вертикальным скроллингом.

Виды графики

У консоли NES есть два вида графики:

  • Спрайты — тайлы, которые можно размещать в произвольных местах экрана и перемещать независимо друг от друга.
  • Фон — сетка тайлов, которые можно плавно прокручивать как единое изображение.

Чтобы продемонстрировать разницу между ними, я покажу сцену, составленную из спрайтов и фона:

А вот та же сцена, в которой видны только спрайты:

А вот сцена, в которой виден только фон:

Скроллинг

Процессор изображений (NES Picture Processor) поддерживает скроллинг фоновых изображений. В видеопамяти графика фона хранится в виде двухмерной сетки тайлов, покрывающей область, в два раза превышающую ширину и высоту экрана.

При постепенном перемещении видимого окна по сетке создаётся эффект плавного скроллинга. На экране отображается «окно» в этой сетке размером с экран, и положением этого окна можно точно управлять.

Сетка тайлов внутри памти представлена как область пикселей размером 512x480 и разбита на четыре прямоугольника размером с экран, которые называются «таблицами имён» (name tables). Выводимый NES видеосигнал имеет размер 256x240 пикселей. Игры могут конфигурировать 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.

Первый спрайт в видеопамяти называют Sprite Zero (нулевым спрайтом). NES одновременно может отрисовывать до 64 спрайтов. Оно задаёт бит в одном из регистров PPU с отображением в память, который может проверяться процессором. В каждом кадре, как только непрозрачный пиксель нулевого спрайта накладывается на непрозрачный пиксель фона, происходит событие Sprite Zero Hit.

Если да, то игра переключается с горизонтального скроллинга для реализации разделения. Чтобы использовать Sprite Zero Hit для разделения экрана, игры располагают нулевой спрайт в вертикальной позиции рядом с границей разделения, и во время рендеринга постоянно проверяют, произошло ли событие Sprite Zero Hit.

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

Внимательнее рассмотрим интерфейс с фоном и без него: Коричневый круг, появляющийся в начале перехода и исчезающий в его конце — это нулевой спрайт.

Нулевой спрайт — это обесцвеченный спрайт бомбы, идеально совпадающий по расположению с обычным спрайтом бомбы из интерфейса игры. Нулевой спрайт настроен так, чтобы отображаться под фоном, но поскольку чёрные пиксели интерфейса считаются прозрачными, бомба нулевого спрайта была бы видимой, если бы не её стратегически не спрятали за бомбой из интерфейса.

Он происходит на верхнем пикселе запала бомбы, который находится в 16 пикселях от низа интерфейса. Заметьте, что Sprite Zero Hit происходит за несколько строк пикселей до нижней строки интерфейса. Когда происходит Sprite Zero Hit, игра начинает считать циклы процессора, и после завершения нужного количества циклов устанавливает горизонтальный скроллинг.

Гашение обратного хода луча

Бóльшую часть времени PPU консоли отрисовывает пиксели на экран. Существует короткий период простоя между кадрами, во время которого отрисовка не выполняется. Это явление называется гашением обратного хода луча (Vertical Blank, или vblank). Некоторые виды изменений конфигурации PPU можно выполнять только во время vblank.

Регистр скроллинга

Игры изменяют позицию скроллинга, осуществляя запись в регистр PPU под названием PPUSCROLL, который отображается на адрес памяти 0x2005. Первая операция записи в PPUSCROLL задаёт компонент X позиции скроллинга, а вторая операция задаёт компонент Y. Аналогичным образом попеременная запись выполняется и дальше.

Компонент Y позиции скроллинга увеличивается раз в два кадра. Ниже показаны все ненулевые операции записи в PPUSCROLL во время этого воспроизведения (в замедленном действии) 16 кадров экрана с сюжетом игры. Все операции записи в PPUSCROLL в этом примере выполняются во время vblank, что заставляет прокручиваться вместе с этим и весь фон.

Скроллинг разделения экрана

Операции записи в PPUSCROLL во время vblank вступают в силу в начале кадра, отрисовываемого непосредственно после vblank. Если позиция скроллинга изменяется во время отрисовки кадра (т.е. не во время vblank), то это изменение вступает в силу, когда отрисовка доходит до следующей строки пикселей. Частичный горизонтальный скроллинг реализуется записью в PPUSCROLL во время отрисовки устройством PPU последней строки пикселей перед скроллингом.

При обновлении позиции скроллинга в середине кадра применяется только компонент X позиции скроллинга. То есть компонент Y позиции скроллинга отбрасывается. Таким образом, если игра хочет разделить экран и меняет позицию скроллинга части кадра, то может выполнять скроллинг только по горизонтали.

И тем не менее:

Хотите верьте, хотите нет, но во время этого перехода значение регистра PPUSCROLL не менялось.

Это баг моего эмулятора, вызванный отсутствием синхронизации тактовых циклов процессора с попиксельным рендерингом. Можно заметить под интерфейсом графический артефакт высотой в один пиксель.

Вмешательство в другие регистры

Второй регистр под названием PPUADDR, отображаемый на адрес памяти 0x2006,, используется для задания текущего адреса видеопамяти. Когда игра, например, хочет изменить один из тайлов в таблице имён, она сначала выполняет запись адреса видеопамяти тайла в PPUADDR, а затем записывает новое значение тайла в PPUDATA — это третий регистр, отображаемый на адрес 0x2007.

при отрисовке кадра) может вызывать графические артефакты. Запись в PPUADDR не во время vblank (т.е. Так как процесс отрисовки на экран выполняется сверху вниз, и слева направо в пределах строки, то PPU по сути присваивает PPUADDR значение адреса текущего отрисовываемого тайла. Так получается потому, что цепь PPU, на которую влияет запись в PPUADDR, также непосредственно управляется устройством PPU в процессе получения тайлов из видеопамяти для их отрисовки. Когда отрисовка переходит от одного тайла к другому, PPUADDR изменяется инкрементом текущего значения.

Таким образом, запись в PPUADDR в середине кадра может изменить тайлы, получаемые PPU из памяти на время текущего кадра.

Поскольку во время перехода таблица имён тоже обновляется, вывод всех операций записи в PPUADDR будет слишком обширным. Давайте выведем операции записи в PPUADDR во время вертикального перехода. При горизонтальном переходе скроллинг задаётся во время отрисовки строки пикселей 63, поэтому рассмотрим операции записи в PPUADDR только во время этой строки.

Чётко видна закономерность. Через каждые два кадра адрес, записываемый в строке пикселей 63, уменьшается на 32 (0x20). Но как это приводит к обновлению фактической позиции скроллинга?

Настоящий регистр скроллинга

Внутри PPU есть 15-битный регистр, не отображаемый в память ЦП. Он используется и как текущий адрес для доступа к видеопамяти, и как конфигурация скроллинга фона.

При работе с этим значением как с адресом бит 14 игнорируется, а биты 0-13 обрабатываются как адрес в видеопамяти.

При работе с этим значением как с конфигурацией скроллинга, разные его части имеют различное значение:

Выбор таблицы имён — это значение от 0 до 3, определяющее текущую таблицу имён, из которой производится отрисовка.

Это текущий отрисовываемый тайл. Грубый скроллинг по X и Грубый скроллинг по Y определяют координату тайла внутри выбранной таблицы имён.

Тайлы — это квадраты со стороной 8 пикселей. Точный скроллинг по Y содержит значение от 0 до 7, определяющее текущее вертикальное смещение строки пикселей внутри текущего тайла.

Существует отдельный регистр, содержащий только горизонтальное смещение текущего пикселя, но он не важен для объяснения того, как в The Legend of Zelda выполняется вертикальный скроллинг. Точный скроллинг по X в этом регистре отсутствует.

Вот первые три операции записи из показанного выше демо. Что происходит с этим регистром, когда игра выполняет запись в PPUADDR?

Разбив записи по адресу на компоненты скроллинга, можно чётко понять, что здесь происходит. Через каждые два кадра уменьшается значение Грубого скроллинга по Y, что приводит к вертикальному скроллингу на один тайл или 8 пикселей.

Это значит, что первые 63 строки пикселей отрисовываются с вершины выбранной таблицы имён, содержащей фон интерфейса. На протяжении каждого кадра изначальное смещение скроллинга равно 0,0, после чего на строке пикселей 63 выполняется запись по адресу. Так как вертикальный скроллинг уменьшается через каждые два кадра, это даёт ощущение вертикального скроллинга части экрана. Однако 64-я строка пикселей и далее отрисовываются с вертикальным скроллингом, применённым из этого адреса.

Скроллим вниз, чтобы скроллить вверх

The Legend of Zelda не может скрыть этот трюк от игроков полностью. Он создаёт видимый артефакт на вертикальных переходах экрана, которые заметен, если присмотреться. При переходе между комнатами первый кадр анимации скроллинга скроллится вниз. Вот анимация в очень замедленном действии.

В режиме таблиц имён видно, что происходит на самом деле. Хотя игрокам может казаться, что видимая область плавно скроллится вверх, переход скроллинга начинается с перемещения видимой области из верхней левой таблицы имён в нижнюю левую таблицу, которая содержит копию фона комнаты. Это необходимо, потому что интерфейс в верхней части экрана тоже является частью таблицы имён, и если бы видимая область скроллилась вверх из исходной позиции, то она бы проходила по интерфейсу.

Самым первым записывается значение 0x2800. Вертикальный скроллинг реализован записью в регистр PPUADDR в середине кадра. Два кадра спустя записывается 0x23A0, а затем значение начинает уменьшаться на 32 каждый второй кадр.

Запись значения 0x2800 в регистр PPUADDR присваивает Выбору таблицы имён значение 2, что приводит к рендерингу нижней левой таблицы имён. Поскольку оба значения скроллинга равны 0, он начнётся с верхнего левого тайла этой таблицы имён. Однако Точный скроллинг по Y равен 2, поэтому присутствует двухпиксельное вертикальное смещение от верха нижней левой таблицы имён. Именно поэтому в самом первом кадре перехода мы видим внизу экрана чёрную полосу высотой в 2 пикселя. Изначальное значение скроллинга для анимации перехода смещено на 2 пикселя вниз, чтобы переход был бесшовным.

Это переносит нас обратно к верхней левой таблице имён, и мы рендерим с 29-й строки тайлов, то есть самой нижней. Два кадра спустя в PPUADDR записывается значение 0x23A0. В Точном скроллинге по Y по-прежнему содержится 2.

Почему бы игре просто не записать 0x0800 и 0x03A0, чтобы не страдать от двухпиксельного смещения? Почему необходимо присваивать Точному скроллингу по Y значение 2?

Каждый тайл в таблице занимает один байт видеопамяти (на самом деле они являются всего лишь индексами в другой таблице), и порядок тайлов и таблиц имён в видеопамяти таков, что Выбор таблицы имён, Грубый скроллинг по Y и Грубый скроллинг по X составляют смещение тайла внутри области памяти с таблицами имён. Четыре таблицы имён занимают область 4 КБ в адресном пространстве PPU, от 0x2000 до 0x2FFF. И это не совпадение! То есть взяв младшие 12 битов внутреннего регистра PPU и прибавив их к 0x2000, можно найти адрес тайла в видеопамяти. Именно так и должен обрабатываться регистр: и как регистр адреса, и как регистр скроллинга.

Но здесь есть один изъян.

Во время рендеринга PPU постоянно переписывает регистр адресом текущего отрисовываемого тайла. При обработке в качестве регистра адреса биты 12 и 13 считаются частью адреса. Так как тайлы расположены в таблицах имён, а таблицы расположены в области памяти с 0x2000 до 0x2FFF, PPU присваивает регистру значения из этого интервала.

Любые байты, которые ему доведётся считать, будут восприниматься как тайлы, что с большой вероятностью приведёт к нежелательным результатам. Когда игра выполняет запись в PPUADDR в середине кадра, если она не запишет адрес тайла в таблице имён, то PPU попытается выполнить считывание откуда-то ещё в видеопамяти. Взяв каждое число в этом интервале и учитывая его компоненты скроллинга, значение Точного скроллинга по Y всегда должно быть равно 2. Поэтому все значения, записываемые посередине кадра в PPUADDR, должны находиться в интервале от 0x2000 до 0x2FFF.

The Legend of Zelda выполняет перемещение по 4 пикселя за кадр при горизонтальном скроллинге, но по 8 пикселей через кадр при вертикальном скроллинге, и теперь мы знаем, почему. Это ограничение означает, что мы не можем изменять Точный скроллинг по Y посередине кадра, то есть при использовании этого трюка для реализации вертикального скроллинга разделения экрана мы ограничены скроллингом по 8 пикселей за раз и всегда двухпиксельным вертикальным смещением от границы тайла.

Артефакт также заметен при скроллинге между комнатами вниз, но в этом случае он возникает в конце анимации.

Дополнительное чтение

  • NesDev Wiki — бесценный ресурс для изучения аппаратной части NES. В частности, к теме этого поста относятся страницы о скроллинге PPU
    и регистрах PPU.
  • Мой по-прежнему очень недоделанный эмулятор NES выложен здесь.

Примечания

Пока я не узнал о внутреннем регистре PPU, мой эмулятор показывал при вертикальных переходах экрана The Legend of Zelda эффект стирания.

Спрайт Линка переносился вниз экрана, как и должно быть, но фон не скроллился. Стирание вызывалось тем, что игра постепенно обновляла таблицу имён, чтобы она содержала графику новой комнаты, но не обновляла скроллинг, чтобы хранить обновления за пределами экрана.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть