Разбор полётов: демо High Hopes на NES

by Shiru 11'15 mailto:shiru at mail dot ru



За редкими исключениями, игровые приставки не завоевали сколько-нибудь заметной популярности в качестве демо-платформ. Эту участь разделяет и NES, оставаясь лишь изредка всплывающей в демо-конкурсах в категориях wild или oldschool экзотикой, а большинство работ для неё в основном примечательно самим выбором платформы. За многие годы существования сцены любительской разработки для NES, демо, в отличие от игр, было написано немного. На Pouet их перечислено всего десять. Наиболее высоко оценённым и популярным из них до сих пор заслуженно является High Hopes, занявшее на Assembly 2007 первое место в категории oldskool demo. На его примере сегодня мы разберём, какие особенности и хитрости применяются при реализации классических демо-эффектов на NES.





Общая информация

Авторы демо – братья thefox и ruuvari из финской группы aspekt, действовавшей в 2002-2007 годах на платформах C64 и PC. High Hopes их единственная работа для NES, также она остаётся последней работой группы в целом.

Основной автор демо thefox, он написал код большинства эффектов и инструментарий. На момент создания High Hopes он не имел большого опыта программирования под NES. Впоследствии он занялся созданием инструментария для разработки для NES, в том числе сделал заметный вклад в популяризацию применения в этих целях языка C, а также написал игру Streemerz, ремейк Flash-римейка одноимённой игры для NES со знаменитого сборника Action 52. К сожалению, озвученные им планы по созданию нового демо до сих пор не осуществились. Сейчас он занимается проектом движка с открытым исходным кодом для написания игр на NES со скроллингом.

Его соавтор, ruuvari, являлся основным автором C64-релизов группы, в том числе и по части кода, но в High Hopes он выступил на вторых ролях, написав музыку, сделав большую часть графики, а также небольшую часть эффектов (тоннель, вращение координат при помощи матриц). Сейчас он продолжает писать музыку для демо и музыкальных конкурсов для PC. На NES-сцене активности ни до, ни после High Hopes не проявлял.

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

Исходный код демо не публиковался. Весь разбор выполнен с помощью средств отладки в эмуляторах FCEUX и NintendulatorDX, включая анализ состояния видеопамяти и видеосистемы в разные моменты времени, и немного трассировку выполняемого кода. Несколько моментов, которые я не смог до конца разобрать самостоятельно (детали синтезатора речи) или в которых не был уверен, помог прояснить сам thefox.



Конфигурация

Европейские корни демо проявляются в том, что оно написано для PAL-версии приставки, отличающейся существенно большей длительностью кадрового гасящего импульса (VBlank), во время которого возможен доступ к видеопамяти при разрешённом отображении графики, и меньшей частотой кадров, что даёт немного больше времени процессора на каждый отдельный кадр. На других версиях приставки демо не работает. Такой подход вполне оправдан для разовой демонстрации на крупном мероприятии, но невыгоден с точки зрения доступности массовому зрителю, так как PAL-версия приставки не совместима ни с гораздо более распространёнными оригинальными NTSC-версиями, ни с многочисленными клонами типа Денди. Эта ситуация немного напоминает историю с Pentagon 128 и более точными клонами ZX Spectrum 128 на отечественной демосцене, но в данном случае стать шансов демо-стандартом у PAL-версии платформы нет, по причине её малой распространённости.

За исключением анимации с шестерёнками, все эффекты в демо 'фреймовые', то есть экран обновляется со скоростью телевизионной развёртки – 50 кадров в секунду.

Демо использует 128 килобайт ПЗУ кода и 64 килобайта ПЗУ графики. Память заполнена примерно на две трети, свободно около 45 килобайт кода и 16 килобайт графики. Память управляется маппером MMC3. Как ни странно, но самая интересная его возможность, генерация строчных прерываний, в демо никак не применена, задействовано только программное управление форматом тайловой карты и переключением банков памяти. Все тайминги выдерживаются точно рассчитанным временем выполнения кода. Со слов thefox, это было сделано для того, чтобы эффекты могли работать и в базовой конфигурации, без маппера (NROM) – вероятно, в целях упрощения их отладки. Возможно по этой же причине для хранения графики используется ПЗУ, а не более ожидаемое в демо ОЗУ.

Карта использования памяти выглядит следующим образом. Примерное распределение банков кода:

PRG0 – twister
PRG1 – куб
PRG2 – анимация шестерёнок, 3D-объекты
PRG3 – kefren bars
PRG4 – появление картинок
PRG5 – синтезатор речи
PRG6 – тоннель
PRG7 – музыка


Использование памяти

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

Наборы тайлов



Музыка

Музыка написана в ранней версии собственного музыкального кросс-редактора, разработанного thefox. На тот момент он назывался Pornotracker. Существенно улучшенная версия редактора стала доступна для публики через три года после выхода демо, а ещё через пару лет автор переименовал его в Musetracker, мотивируя это решение желанием включить упоминание редактора в своё официальное резюме.

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

Ударные в треке классические чиптюновые, тон и шум. DPCM не используется, так как он сбивал бы выдерживаемые программными задержками точные тайминги эффектов – чтение каждого байта останавливает процессор на 1-4 такта, этот процесс асинхронен, а точная длительность задержки зависит от многих факторов.

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



Голос

Пожалуй, самой примечательной фишкой демо можно назвать синтезатор голоса, проговаривающий некоторые надписи на экране в духе речевых синтезаторов начала 1980-х, типа Currah Microspeech, и их многочисленных программных аналогов (Speech, Hlasovy Program, Beszed, Kecal, Fongen и т.п.). Это не просто заранее заготовленные сэмплы со всеми фразами. Слова составляются из отдельных фонем финского языка, записанных самим автором.

Сэмплы фонем хранятся в виде обычного 8-битного PCM с частотой дискретизации около 9 килогерц. Некоторые из них могут быть зациклены для увеличения длительности звучания (гласные), некоторые играются без зацикливания (согласные). Всего в программе примерно 12 килобайт сэмплов. Произносимый текст закодирован в виде индексов фонем и бита для опционального удвоения длительности зацикленных фонем.

Голос воспроизводится, когда в музыке пауза или висит протяжная нота, действия на экране в эти моменты нет. Сэмплы проигрываются прямым выводом в 7-битный ЦАП APU (регистр $4011), DPCM здесь также не используется.

К синтезируемому голосу применяется эффект задержки с обратной связью, то есть эхо. Для него используется кольцевой буфер размером во всю свободную во время проигрывания звука оперативную память, 3328 байт, что даёт длительность эхо примерно в треть секунды. Для каждого отсчёта читается новая выборка из ПЗУ, к ней прибавляется поделённая на 4 задержанная выборка из кольцевого буфера, результат сначала записывается обратно в буфер, потом преобразуется в беззнаковое 7-битное значение и выводится в ЦАП. Вместе с задержкой из 43 NOP'ов этот процесс занимает около 180 тактов для обеспечения требуемой частоты дискретизации.



Появление картинок

Появление картинок

Классический растровый эффект, название которого, к сожалению, мне до сих пор неизвестно. Одно время он нередко встречался в TR-DOS адаптациях игр на ZX Spectrum, в горизонтальных, вертикальных, односторонних и двухсторонних вариациях.

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

Появление картинок

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

Самое сложное при реализации этого эффекта на NES – установка нового вертикального смещения тайловой карты перед началом отображения каждой строки телевизионного растра, чтобы поместить на отображаемую строку требуемую строку исходного изображения. Сложность заключается в обеспечении точных таймингов и понимании наиболее нетривиальной особенности устройства видеосистемы NES, подробное рассмотрение которой заслуживает отдельной статьи. Здесь же в общих словах можно сказать, что для каждой строки растра во время строчного гасящего импульса (HBlank) нужно провести четыре записи в регистры PPU заранее подготовленных значений с хитроумно разбросанными и перемешанными битами горизонтального и вертикального смещений, причём времени для четырёх записей недостаточно, но первые две записи можно выполнить до начала HBlank. Сам код записи в эти регистры выглядит очень просто, вся хитрость в понимании того, что нужно записать:

;в X и Y заранее загружены нужные значения для установки вертикального смещения

lda #$00	;два регистра устанавливаются в 0, потому что горизонтальное смещение в этом эффекте не меняется
sta $2006
stx $2005
sta $2005	;эту и следующую запись нужно выполнить во время HBlank
sty $2006
lda $001c	;эти три команды выбирают нужный набор тайлов
ora $9100,x
sta $2000




Kefren bars

Kefren bars

Совершенно классический и простой на первый взгляд эффект обрёл довольно нетривиальную реализацию на NES.

Непонятный набор точек и полосок в предпоследнем (одиннадцатом) наборе тайлов нужен именно для этого эффекта. Каждый тайл содержит монохромную полоску пикселей 8x1. Она находится в третьей пиксельной строке тайла, с целью оптимизации кода установки строки, речь о котором пойдёт немного ниже. В наборе тайлов последовательно перебираются все возможные комбинации пикселей в этой полоске (младший бит слева), которых всего 256, как и тайлов в наборе. Таким образом создаётся возможность задать с помощью имеющихся тайлов произвольное монохромное изображение в пределах одной строки растра. Оно формируется 32 байтами в тайловой карте, при этом формат строки полностью совпадает с ZX Spectrum, так как индексы тайлов в наборе соответствуют бинарному представлению этих индексов, изображённому в виде полоски пикселей в тайлах.

К сожалению, таким образом не получится вывести произвольное полноэкранное изображение, потому что в тайловой карте всего 60 строк, и даже если расширить её с помощью дополнительной памяти на картридже, получится только 120 строк. Обновить же целую строку тайловой карты во время прохода луча по растру невозможно, это не позволяет сделать устройство видеоконтроллера, и не хватит скорости процессора – в строке 106 тактов (для PAL), а на обновление 32 байт нужно минимум 192 такта (6 тактов на байт).

Однако, сам принцип работы эффекта kefren bars делает его возможным на NES. При ширине перемещающейся центральной полоски эффекта в 8 пикселей или меньше достаточно обновлять всего два байта в каждой строке, а остальные байты остаются от предыдущей. Именно так и работает эффект в демо – в строке из 32 байт в тайловой карте по адресу $2800 каждую строку растра во время HBlank обновляется два байта, после чего смещение для отображения устанавливается на начало тайловой карты. Так как обновляемая строка находится всегда в одном и том же месте тайловой карты, в отличие от предыдущего эффекта появления картинки, где каждая строка имела своё попиксельное смещение, достаточно всего двух записей в регистр текущего адреса. Получается следующий код для HBlank:

;Y=$28, старший байт адреса тайловой карты
;A=смещение в строке тайлов

ldx #$00		;запрещаем отображение, чтобы скрыть артефакты
stx $2001
sty $2006		;устанавливаем старший байт текущего адреса PPU в $28
sta $2006		;устанавливаем младший байт текущего адреса PPU в нужное смещение
lda #$06		;первый байт 'графики'
sta $2007		;отправляем в видеопамять
lda #$18		;второй байт 'графики'
sta $2007		;отправляем в видеопамять
sty $2006		;сбрасываем старший байт текущего адреса PPU на начало тайловой карты, $28
sty $2001		;разрешаем отображение
stx $2006		;сбрасываем младший байт текущего адреса PPU на начало тайловой карты, $00


Адрес начала тайловой карты $2800 выбран не случайно. Он используется для трюка с разрешением отображения после изменения видеопамяти и установки начала отображаемой строки. Трюк позволяет избежать лишней загрузки регистра, занимающей два такта, используя значение $28 как маску разрешения отображения – 00101000. Установленные биты включают отображение слоя фона (бит 3) и усиление красного канала (бит 5), которое немного изменяет отображаемый на экране цвет. Другой трюк – выполнение этого фрагмента кода из ОЗУ. Это позволяет загружать байты графики в регистр A в виде непосредственных значений, а не чтения из адреса, что экономит ещё два такта.

Таким образом вся последовательность манипуляций с видеоконтроллером и видеопамятью укладывается в 38 тактов, а от запрещения до разрешения отображения проходит 28 тактов. Длительность HBlank составляет 28 тактов.



Twister

Twister

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

В одной тайловой карте и двух набора тайлов (первый и второй) подготовлена текстура твистера. Каждый набор тайлов содержит половину текстуры, высотой 15 тайлов. В принципе можно было бы обойтись и одним набором, так как половины отличаются очень незначительно, буквально несколькими пикселями дитеринга.

Код для HBlank очень похож на тот, что используется для эффекта появления картинок:

;в X заранее загружено значение для установки вертикального смещения
;в Y маска разрешения отображения, так как эффект по высоте меньше экрана и часть строк гасится

lda #$00		;горизонтальное смещение не меняется
sta $2006
stx $2005
sta $2005
lda $9500,x		;второе значение для установки вертикального смещения берётся из таблицы
sta $2006
sty $2001		;разрешение отображения
lda $0014		;выбор нужного тайлового набора
sta $2000
Текстура твистера, как она выглядит в тайловой карте:

Текстура твистера



Тоннель

Тоннель

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

Для отображения эффекта используется некоторое подобие 'чанков'. Одна чанковая точка имеет размер 4x4 пикселя и может иметь один из четырёх цветов. Пятый набор тайлов содержит перебор всех возможных комбинаций чанковых точек в пределах одного тайла – как и тайлов, их всего 256 (четыре точки по два бита). Получается, что один байт карты тайлов задаёт цвет четырёх точек. Никаких растровых трюков здесь не требуется, просто обычное отображение слоя фона.

Алгоритм эффекта готовит изображение в ОЗУ в диапазоне адресов $0200..$037f, что составляет 384 байта. Это изображение верхней половины тоннеля, уже готовое к пересылке в тайловую карту. Нижняя половина получается вертикальным и горизонтальным отзеркаливанием верхней. Во время VBlank изображение из ОЗУ передаётся в видеопамять полностью развёрнутым циклом с абсолютными адресами источника – сначала прямым копированием в порядке возрастания адресов от $0200 до $037f, потом в обратном порядке от $037f до $0240 через LUT для отзеркаливания чанковых 'пикселей' в тайле. Эти варианты кода выглядят так:

lda $0xxx		;читаем готовое значение из буфера по абсолютному адресу
sta $2007		;передаём в видеопамять
ldx $0xxx		;читаем значение из буфера в индексный регистр
lda $aa00,x		;читаем значение из таблицы отзеркаливание для этого тайла
sta $2007		;передаём в видеопамять


Первый вариант затрачивает 8 тактов на байт, второй 12 тактов на байт. Всего таким образом передаётся 384 и 320 байт соответственно, итого 704 байта. На всю передачу уходит 6912 тактов, при длительности VBlank в PAL-версии приставки 7420 тактов (для сравнения, 2260 в NTSC). Некоторая несимметричность изображения, у которого верхняя половина немного выше отзеркаленной нижней, объясняется оптимизацией времени пересылки, так как передача отзеркаленного тайла выполняется в 1.5 раза медленнее.

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



Шестерёнки

Шестерёнки

Принцип вывода этого эффекта на экран точно такой же, как у предыдущего, и для него используется тот же набор тайлов. Но теперь это действительно чистая анимация. В ней всего пять кадров, размером во весь экран. Их очертания хорошо видны в начале банка PRG2 на карте использования памяти.

Это единственный эффект в демо, не укладывающийся во фрейм, так как даже увеличенной длительности VBlank PAL-версии приставки недостаточно для обновления всех 1024 байт. Поэтому обновление происходит за два кадра. За один кадр передаётся 512 байт. Передача данных в видеопамять сделана на первый взгляд довольно неоптимально, обычным циклом:

lda ($01),y		;чтение байта
sta $2007		;запись в видеопамять
iny				;увеличение младшего байта адреса чтения
bne $8046		;проверка, не нужно ли увеличить старший байт


Получается 13 тактов на байт, плюс ещё пять тактов на 256-ой байт, когда происходит увеличение старшего байта адреса чтения. Итого 6661 такт. Развёрнутый цикл дал бы экономию в два такта на байт за счёт убирания bne, но остальная часть осталась бы такой же, так как другого способа читать данные с косвенной 16-битной адресацией, чтобы не держать копию кода для каждого кадра, нет. Таким образом разворачивания цикла оказалось бы недостаточно, чтобы уложиться в один кадр, а неразвёрнутый цикл в этом случае при той же эффективности имеет преимущество в компактности кода.

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



X-rotating cube

X-rotating cube

Ещё один классический эффект. Часто встречается в демо для Commodore 64, но там он обычно не имеет текстуры.

Эффект не масштабирует строки текстуры по горизонтали на лету. Вместо этого в одной тайловой карте и двух наборах тайлов (третий и четвёртый) заранее подготовлено восемь отмасштабированных по горизонтали изображений текстуры для одной стороны куба – в одном наборе тайлов по четыре масштаба. Вертикальное разрешение текстуры получается равным 30 пикселям.

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

;в X и Y заранее загружены значения для установки вертикального смещения

lda #$1e	;разрешение отображения
sta $2001
lda #$00	;два регистра устанавливаются в 0, горизонтальное смещение не меняется
sta $2006
stx $2005
sta $2005	;эту и следующую запись нужно выполнить во время HBlank
sty $2006
lda $9300,x	;эти две команды выбирают нужный набор тайлов
sta $2000


Текстура куба, как она выглядит в тайловой карте:

Текстура куба



Трёхмерные вращающиеся объекты

Трёхмерные объекты

Единственный не-растровый эффект в демо. Представляет собой именно то, чем выглядит на первый взгляд – трёхмерные фигуры вращаются в реальном времени с помощью матричных преобразований, с не очень высокой точностью (8-битная математика), после чего вершины выводятся спрайтами.

Объекты состоят примерно из 40-60 вершин. В одном из них их даже 66, и на одновременный вывод их всех не хватает аппаратных спрайтов, поэтому две вершины пропадают. Я не пытался разбирать код вращения, но учитывая, что в кадре примерно 30000 тактов и на одну вершину можно затратить до 450 тактов, коду вращения не требуется быть сверх-оптимальным. По моим замерам реально в эффекте на обсчёт одной вершины уходит около 300 тактов, включая проецирование на экран и вывод спрайта. По информации thefox, на само вращение уходит 150-200 тактов.

Графика спрайтов находится в десятом тайловом наборе, с маленьким шрифтом. Сам этот шрифт в демо нигде не встречается и вероятно использовался для отладочных целей. Всего для отображения вершин используется семь тайлов 8x8, выглядящих как окружности разного размера. Нужный тайл выбирается в зависимости от Z-координаты вершины после вращения. Сортировки вершин по дальности при выводе нет, спрайты дальних вершин иногда выводятся поверх ближних. Так как вращение быстрое, а спрайты пересекаются редко, и, будучи почти однотонными, визуально почти сливаются, это практически незаметно. У всех спрайтов одинаковая палитра, что оставляет потенциал для улучшения эффекта. Палитры разной яркости для разного отдаления вершин усилили бы эффект объёмности, но отсутствие сортировки стало бы более заметным.

Так как объекты довольно крупные, в основном объёмные (выпуклые, вершины распределены в пространстве), спроецированные на экран вершины большую часть времени находятся далеко друг от друга. Переполнение лимита спрайтов происходит редко и поэтому почти незаметно. Системы мерцания спрайтов не предусмотрено, вышедшие за лимит спрайты просто не отображаются. Больше всего это проявляется на втором объекте, торе, так как он более плоский по одной из осей, и в определённые моменты его вращения множество спрайтов оказывается на одной строке.

Надписи во время эффекта выводятся на слое фона. Их шрифт находится в девятом тайловом наборе. Он же используется для всех надписей в демо. Размер символов в шрифте 16x32 точки, то есть 2x4 тайла. Появление и гашение надписей сделано палитрой. Замена надписей происходит в момент, когда они чёрные и сливаются с цветом фона. Для замены надписи требуется обновить четыре строки тайловой карты, то есть 128 байт. Обновление происходит во время VBlank, его длительность в PAL-версии приставки позволяет без проблем провести такое обновление за один кадр самым тривиальным кодом.