Обзор графической архитектуры Famicom/NES/Денди

Про Famicom/NES/Денди уже написано и рассказано очень много, включая те сведения, что я напишу ниже, включая на этом же самом сайте. Но раз уж у меня получился цикл статей про графику в разных консолях, то обойти вниманием денди из детства не представляю возможным. Так что повторим еще раз как устроена с точки зрения программиста денди…



Центральный процессор

В качестве ЦП в Famicom/NES/Денди трудится чип Ricoh 2A03 на частоте 1,66 или 1,79 МГц (в зависимости от региона) — немного переработанный процессор на архитектуре MOS 6502. У ЦП есть 64Кб адресного пространства, которое имеет следующую раскладку (диапазоны адресов даны в 16–ричной системе счисления, в скобках — размер диапазона в байтах):

Первая половина RAM (32 Кб):

0000–1FFF (8192) — 2048 байт ОЗУ (зеркалированы 4 раза)
2000–3FFF (8192) — порты ввода–вывода к PPU (зеркалированы 1024 раза)
4000–401F (32) — прочие порты ввода–вывода (звук, геймпад и т.п.)
4020–5FFF (8160) — дыра 1 (см. ниже)
6000–7FFF (8192) — дыра 2 (см. ниже)

Вторая половина RAM (32 Кб):
8000–FFFF (32768) — ПЗУ картриджа


Итак, консоль имеет всего 2Кб встроенной ОЗУ, причём она «зеркалирована» на 8Кб адресного пространства — то есть, например, чтение/запись ячейки 0 равносильно чтению/записи ячеек 2048, 4096 или 6144. На деле это означало всего лишь то, что для удешевления аппаратуры контроллер ОЗУ игнорирует некоторые линии адресной шины, таким образом адреса $800–$1FFF потеряли практическое значение.
Аналогичным образом следующие 8Кб «потрачены» всего на восемь однобайтовых порта ввода–вывода к PPU.
Может показаться расточительным или даже глупым такое «бездарное разбазаривание» ценного в 8–битках адресного пространства — полезные вещи занимают в первой его половине крайне мало места, да и количество ОЗУ — всего 2 Кб — кажется смехотворным.
Однако Famicom, появившаяся в 1983 году и не обязана была использовать всю возможности архитектуры MOS 6502. Крайне немаловажным фактором для коммерческого продукта была цена, а цена килобайта памяти тогда была очень высокой. Кроме того у консоли не стояла цель достичь горизонтов, но всего лишь превзойти конкурентов. И она превосходила их многократно!
Так, у самой популярной консоли предыдущего поколения — Atari 2600 было всего 128 байт ОЗУ и 4Кб ПЗУ. А одна из лучших игр на ней — Pitfall — выглядела вот так:



И это еще один из лучших представителей! Один же из худших («E.T.» — игра с которой прочно ассоциируется начало кризиса видеоигр в 1983 году) выглядел так:



Даже игры первых лет на Famicom выглядели заметно опрятнее:



Что же касается того какими они станут к закату консоли — я думаю, что даже создатели на момент выхода не подозревали истинного потенциала:



Тем не менее небольшое количество ОЗУ даст о себе знать и заметно повлияет на геймплей многих игр на денди. Так в подавляющем большинстве платформеров и боевиков экран либо не скроллится назад, либо при возврате на места где игрок уже бывал все монстры возрождаются как ни в чём не бывало. Это скрадывает то, что 2Кб ОЗУ в денди маловато для хранения информации о лабиринте и данные как правило берутся из ПЗУ как только они попадают в активную область экрана.
В играх первых лет незанятые диапазоны («дыры») в первой половине памяти не использовались вообще. Позже с появлением мапперов некоторые продвинутые игры стали использовать «дыру 2» для отображения дополнительного ПЗУ, ОЗУ или чипа для сохранения данных игры (последние две вещи располагались на картридже, удорожая его). Например в таком платформере как Felix The Cat монстры не возрождаются — потому что на картридже игра несёт 8Кб дополнительного ОЗУ (а это почти в два раза больше, чем консоль имеет суммарно), отображенного в «дыру 2». «Дыра 1» как я понял очень редко использовалась для чего то полезного, например маппер MMC5 отображал в неё порты ввода–вывода для управления собой.
Так или иначе на момент выхода Famicom уже не могла удивить владельцев топовых компьютеров характеристиками процессора или ОЗУ, поэтому брала она сравнительной дешевизной, простотой использования и навороченной графикой. За «графоний» в Famicom отвечает PPU — Picture Processing Unit, который можно назвать аппаратным ускорителем тайловой графики.

PPU

PPU обладает собственным адресным пространством со следующей раскладкой:

0000–1FFF (8192) — Две таблицы тайлов (Pattern tables 1,2)
2000–2FFF (4096) — Четыре таблицы фонов (Name tables 1,2,3,4)
3F00–3F1F (32) — Таблицы цветов для фона и спрайтов

Кроме того в отдельном банке памяти в PPU хранится 256 байт таблицы спрайтов — Object Attribute Memory (OAM).

Картинка, которую выводит NES обладает эффективным разрешением 256x240 пикселей, но минимальной единицей изображения в Famicom/NES/Денди является тайл — картинка размером 8x8 пикселей. Тайлы в денди четырёхцветные, то есть содержат по два бита на пиксель, что требует 16 байт для описания одного тайла. Эти описания тайлов должны маппится с микросхем картриджа (они называются CHR–ROM или CHR–RAM) в первые 8 Кб адресного пространства PPU — в две таблицы тайлов (pattern tables). Каждая из этих двух таблиц содержит 256 описаний тайлов, что требует 4096 байт на каждую. Одна из таблиц (на выбор) используется для тайлов фона, другая же — для тайлов спрайтов. Вот как они выглядят в отладчике эмулятора FCEUX (игра Lode Runner):



Вообще формируется изображение из двух составляющих: фона — квадратно–гнездовой сетки из тайлов, которая может скроллится в разных направлениях и спрайтов — до 64 произвольно расположенных над (или под) фоном тайлов. Спрайтам и фону можно назначить разные таблицы тайлов (как правило так и делалось) и таким образом на экране одновременно можно отобразить до 384 уникальных тайлов — 256 на фоне и 128 через спрайты в режиме 2 тайла на спрайт, остальные должны так или иначе повторяться. То есть из 61440 пикселей экрана в денди «из коробки» только 24576 могли быть уникальными, что представляло проблему, например, при создании заставок — тайлы так или иначе должны были повторяться и их обязательно надо было переиспользовать в картинке. Этой проблеме посвящена замечательная статья от Shiru на нашем же сайте.

Тайлы строго говоря являются трёхцветными, так как нулевой цвет в них является прозрачным (даже в тайлах фона). Если над пикселем экрана не оказалось ни одного непрозрачного пикселя (ни спрайтов ни фона), то он окрашивается глобальным «цветом задника». Так или иначе четыре цвета для всей картинки — это маловато, поэтому значения бит в пикселях тайлов являются не конечными значениями цвета, а индексами в таблицах цветов из глобальной палитры.
Глобальная палитра в Денди содержит 64 цвета из которых 56 являются уникальными (а некоторые вообще не рекомендуется использовать). Вот она:



Но в тайлах хранятся даже не эти цвета, а индексы в таблицах цветов, ячейки которой уже на них ссылаются.
По адресу 3F00 в PPU хранятся подряд восемь четырёхбайтовых таблиц цвета. Первые четыре их них составляют четыре возможных палитры для тайлов заднего фона, последние же четыре — для тайлов спрайтов. Один и тот же тайл в денди может отображаться на экране в разных местах с разными палитрами — для этого фон и спрайты добавляют к информации о тайлах разными способами по два бита селектора палитры, о чём будет рассказано ниже. Хрестоматийным уже примером переиспользования тайлов с разными палитрами являются тайлы облаков и травы в Super Mario Bros:



Если приглядеться к облакам и траве, то можно понять, что изображения в них используются одинаковые, отличаются только цвета — то есть палитры. Подменой или модификацией палитр на денди достигаются многие полезные трюки. Например в TMNT 3 большинство антропоморфных рядовых врагов использует одни и те же тайлы, а различаются они преимущественно цветом — то есть одинаковые тайлы спрайтов окрашиваются разными палитрами. А мигающие при ударе боссы делаются просто быстрой подменой значений в таблице цветов.
В штатном режиме из 64 возможных цвета в одном кадре денди может присутствовать не более 25 — четыре таблицы по три непрозрачных цвета для фона и столько же для спрайтов дают 24 возможных разных цвета тайлов, плюс один глобальный цвет задника.

Задний фон

Задний фон в Famicom/NES/Денди описывается так называемыми «таблицами имен (name tables)». Таблица имён/фона — это массив байт размером 32x30 ячеек в каждой из которых хранится индекс тайла из соответствующей таблицы тайлов (pattern table). Номер таблицы тайлов, используемой для фона задаётся в регистрах PPU. Таким образом изображение одного тайла может быть многократно переиспользовано в заднем фоне по полной аналогии с изображением символа в текстовых видеорежимах персональных компьютеров — таблица тайлов тут соответствует данным с изображением растра шрифта, а таблица имён/фона соответствует видеостранице текста в которой указываются лишь номера символов в соответствующих ячейках квадратного поля. Поэтому неслучайно то, что в руководстве по программированию под свою намного более позднюю консоль Game Boy Advance Nintendo называет тайловые видеорежимы «текстовыми».
Под одну таблицу имён отводится 1024 байта, но 32*30=960 — остаётся еще 64 неиспользованных байта. Вот их то и пустили под двухбитовые селекторы палитр для тайлов на которые таблица имён ссылается. Но тут вышла закавыка — если попытаться запаковать в байты 32*30*2=1920 бит, то потребуется 240 байт, а у нас есть только 64. Пришлось сохранить в четыре раза меньше бит, на что хватает 60 байт, поэтому селектор палитры в фоне указывается не для каждого тайла по отдельности, а для групп из 2x2 тайла.

Размера одной таблицы фона 32x30 в принципе достаточно, чтобы заполнить фоном весь экран Денди, но разметка памяти сделана под четыре таких таблицы идущих подряд в ОЗУ.
Дело тут скроллинге — в отличие от большинства текстовых видеорежимов в персональных компьютерах задний фон в Денди можно произвольно скроллить как по горизонтали так и по вертикали. При выходе за границы одной таблицы фона видеочип показывает содержимое соседней с краю таблицы, при этом геометрически они стыкуются друг с другом следующим образом:



Здесь еще показаны 16–ричные адреса соответствующих таблиц в адресном пространстве PPU.
На все четыре таблицы фона нужно, очевидно, четыре килобайта видеопамяти (VRAM). Но в целях удешевления в консоль установили только 2Кб VRAM — в адреса $2000–$27FF (зоны A и B на рисунке), на месте же видеостраниц 2800–2FFF (зоны C и D) зияет огромная дыра (как правило она зеркалится на зоны A–B). Однако с помощью коммутации линий адреса в картридже можно поменять раскладку имеющихся двух килобайт VRAM так, чтобы второй из них оказался не в зоне B, а в зоне C (тогда уже зоны B и D зеркалили зоны A и C), то есть видеопамять «вытягивалась в высоту». В первом режиме было удобно делать горизонтальные скроллеры. Во втором — вертикальные.

Типичные скроллеры пользовались тем, что когда правый край экрана уходил еще правее зон B или D отображение его «проворачивалось и вылезало» слева в зоны A или C. При этом оказывалось возможным реализовать бесшовный бесконечный скроллинг обновляя всякий раз только узкую полоску тайлов появляющуюся с нужного края.
Вот, например, наглядная демонстрация того как это работало в типичных скроллерах (из статьи (англ.) wiki.nesdev.com/w/index.php/PPU_scrolling):



Здесь рамкой отмечена активная область экрана, а снизу показано итоговое изображение. Обратите внимание, что тайлами с пиктограммой лапки отмечено обновление полоски шириной в два тайла — это не случайно, а потому что так удобнее обновлять еще и атрибуты селекторов палитр, ведь они применяются к блокам 2x2 тайла.
Переключением отображения VRAM в вертикальное столь же удобно делать бесшовный вертикальный скроллинг.
Некоторые игры по ходу уровня переключаются между вертикальным и горизонтальным скроллингом — например Super Contra или Darkwing Duck.
Но появляются проблемы, когда в игре нужно совместить и горизонтальный и вертикальный скроллинг одновременно. Тогда по одному из измерений нередко начинает происходить конфликт атрибутов — некоторые тайлы с краёв могут отображаться в неправильной расцветке — это происходит из–за перекрещения двух слабых мест денди — нехватки видеопамяти для двух видов скроллинга одновременно, в связи с чем «проворачивание» по одному из измерений происходит слишком близко к визуальной границе экрана, и то, что селектор палитры есть не у каждого тайла фона, а у крупных блоков 2x2, что на этой границе и становится заметно (например в игре Super Mario Bros 2, где бесшовным был вертикальный скроллинг, а при горизонтальном возможны были конфликты атрибутов на правой границе экрана).
В принципе картридж мог включать в себя дополнительные 2Кб VRAM для организации бесшовного скроллинга во всех направлениях, но такое в играх использовалось крайне редко.

Так как ЦП и PPU обладают независимыми адресными пространствами, то ЦП передавать данные в VRAM может только через порты ввода–вывода. Причём существуют отдельные порты для передачи/получения данных из адресного пространства PPU и OAM. Например при записи в адресное пространство PPU в одном регистре выставлялся адрес куда нужно было записывать, а в другой производились записи байт. При этом при каждом обращении адрес записи автоматически продвигался вперед на 1 или 32 байта, что управлялось флагами.
Однако всегда нужно было помнить о том, что видеочип настолько плотно блокирует всю свою память на период VDraw, что доступ к ней возможен только на периоды VBlank.
Размеры VRAM очевидным образом диктуют размеры игровых пространств в некоторых играх (особенно это типично для игр первых лет), таких как Bomberman или Lode Runner. Так, например, выглядит содержимое таблиц фона в Bomberman в эмуляторе FCEUX:



Спрайты

В Famicom/NES/Денди возможно 64 аппаратных спрайта, описания которых хранятся в таблице OAM размером 256 байт в отдельном чипе памяти PPU. Доступ к ней ЦП осуществляется или посредством специализированнх портов ввода–вывода или посредством DMA–контроллера, который собственно умеет только одно — передать 256 байт из памяти ЦП в память OAM (зато очень быстро). Каждый спрайт описывается четырьмя байтами в которых задаются его координаты на экране, номер изображаемого тайла, селектор палитры, режим зеркального отражения по вертикали и/или горизонтали и приоритет (рисуется спрайт перед фоном или за ним).
Не существует флага включения/выключения конкретного спрайта — для того чтобы спрайт не выводился предлагается просто вывести его за пределы экрана координатами. Порядок заслонения спрайтов спрайтами же диктуется порядком их в таблице.
В одном сканлайне не может быть нарисовано более восьми спрайтов — остальные будут отброшены. Видеочип взводит флаг при наступлении такой ситуации и процессор может переставить спрайты в другом порядке — тогда они начнут характерно перемигиваться, но видны будут всё–таки все.
Существует особый режим отображения спрайтов — 8x16 — когда сразу под выбранным в спрайте тайлом рисуется следующий за ним в таблице тайлов и становится возможным в спрайтах выбирать любой из 512 тайлов из обеих таблиц. Такой режим позволяет более плотно заполнять экран спрайтами.
Тем не менее 512 тайлов на спрайты и фон решительно не хватало для игр выходящих с середины эпохи Famicom, поэтому в эти времена получили широкое распространение мапперы, подменяющие тайловые таблицы на лету и прочим образом расширяющие возможности консоли.

4 комментария

avatar
Диагональный скролл в Super C был засчет тех самых дополнительных 2K VRAM, или там еще какая-то фича?
avatar
Была всего одна игра с дополнительными 2K VRAM для карт тайлов, Gauntlet. Остальные выкручивались как могли. Проблема с диагональным скроллом при двух картах тайлов только в том, как скрыть область дорисовки новых тайлов.

Самый простой вариант диагонального скролла в SMB3 и Felix The Cat — вертикальная раскладка карт тайлов. По вертикали вообще ничего не дорисовывается, уровень всего два экрана в высоту (минус высота панельки снизу). По горизонтали дорисовывается прямо на экране (т.к. карта тайлов в ширину с экран). SMB3 просто не заморачивается по этому поводу, дорисовка видна визуально на краях экрана. Felix скрывает два вертикальных столбца на краях экрана, чтобы сделать дорисовку незаметной — слева аппаратной фичей выключения рендеринга в левом столбце, а справа столбцом чёрных спрайтов (т.к. аппаратной фичи для правого столбца не предусмотрено).

В других играх, где уровень выше двух экранов, стоит вопрос, скрывать вертикальную или горизонтальную дорисовку. Если выбрать горизонтальную раскладку экрана, легко скрыть горизонтальную дорисовку за экраном, но видна вертикальная. Её можно отсечь с помощью панельки или включения/гашения рендера позже начала/раньше конца кадра (обрезав высоту на 16 пикселей). Если не ошибаюсь, с гашением сделано в Jurassic Park. Можно сделать раскладку вертикальной, тогда вертикальная дорисовку получается скрыта, но видна горизонтальная. Её можно скрыть как в Felix, или оставить видимой как в SMB3 (нередко так и делали).

Часто не скрывали дорисовку просто потому что на старых ЭЛТ телевизорах края растра уходили за края видимой области экрана. Потому на всех ТВ-приставках времён ЭЛТ в гайдлайнах есть требования к safe area — к соблюдению области, в которой изображение будет гарантировано видимым. Т.е. за пределами safe area нельзя размещать критичную информацию типа очков или статуса. На NES это отступы примерно по 16 пикселей по вертикали и 8-16 по горизонтали.

Можно сделать горизонтальную раскладку, тогда область горизонтальной дорисовки скрыта, а вертикальной видна.
avatar
И нам нужно редактирование сообщений. Последняя строка лишняя, осталась от перетасовки абзацев. Прямо как артефакт дорисовки.
avatar
В дополнение к комментарию Shiru — посмотрел в эмуляторе на Super C — там используется горизонтальная раскладка A-B, поэтому по горизонтали проблем с прокруткой не могло быть. По вертикали же используется только тот факт, что некоторые телевизоры не отображают полоски сверху и снизу. В эмуляторе FCEUX можно либо выбрать регион PAL либо прямо в опциях NTSC указать отрисовку кадра с 0 до 239 строки (по умолчанию стоит 8 — 231, то есть скрыты по полоске тайлов сверху и снизу как в NTSC физически и происходило) и тогда рваные обновления тайлов сверху/снизу станут видны.
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.