Обзор графической архитектуры Nintendo 64

Nintendo 64 вышла в 1996–ом году и стал конкурентом вышедшей двумя годами ранее Playstation 1.





Центральным процессором в системе был тоже MIPS, но уже на базе 64–битного R4300i на 94МГц. Шина адреса, впрочем была урезана до 32 бит, что логично, ибо 64–битные целочисленные операнды вообще практически не использовались. Повышенная битность тут была больше маркетинговой, чем реальной необходимостью.
В качестве первого сопроцессора у неё стоял так же как и в PS1 контроллер виртуальной памяти, а вот вторым был сопроцессор с плавающей точкой.
Встроенной в консоль основной RAM было 4Мб причём она могла расширяться до 8Мб посредством слота расширения. Отличительной особенностью было то, что байт в этой памяти был 9–битным, правда это мог увидеть и использовать только чип растеризации (RDP), который использовал эти биты для стенсиля (см. ниже). Остальные компоненты системы читали и писали байты только как 8–битовые значения. Еще одна особенность — встроенные 4Мб памяти физически находились на двух разных микросхемах по 2Мб каждая и тот же растеризатор мог работать с ними в параллельном режиме, поэтому, к примеру, официально рекомендовалось располагать фреймбуфер в нижних 2Мб, а Z–буфер — в верхних.
Так что в целом и по битности ЦП и по его частоте и по наличию плавающей точки Nintendo 64 была посовременнее PS1, временная фора свою роль сыграла.
В N64 Nintendo сделала уже тогда непопулярный шаг и в качестве накопителя информации выбрала не оптические диски, а старые добрые картриджи.
Но неожиданным лично для меня оказался тот факт, что в глубоком отличии от консолей предыдущих поколений ROM картриджа больше не мапилось в основное адресное пространство процессора вообще никак.
ROM картриджа стало в N64 ничем иным, как внешним хранилищем к которому обращение идёт через DMA–каналы. Причина этого кроется видимо в крутом повышении частот доступа к основной RAM при которых дешевая ROM картриджей стала сильно отставать — до сотни раз (5 Мб/сек чтения типичного медленного ROM против ~500 у RAM).
В итоге картридж из полноценного ROM стал не более чем внешним накопителем — с быстрым случайным доступом, конечно (и в целом быстрым), но всё же просто внешним накопителем. Так, при старте консоли в RAM копируется первый мегабайт картриджа и в него передаётся управление.

Reality CoProcessor (RCP)

Интересно, что в Nintendo 64 центральную роль в системе играет видеочип — причём сам он по себе является многокомпонентной величиной.
Для того чтобы сделать унифицированную RAM и не разделять банки графической и основной ОЗУ в итоге было сделано так, что ЦП обращался к RAM через видеочип.
И вообще все пути–дорожки сходились именно на видеочипе — как общение с периферией, так и общение хоть с памятью хоть с картриджем как видео, как геймпада, как и звука — всё шло через видеочип.
Он получается становился каким то южным мостом в одном флаконе и точкой, где встречались все компоненты системы.
Весь видеочип в целом назывался RCP (Reality CoProcessor), при этом его можно было поделить на несколько больших и слабо зависимых компонент:
VI (Video Interface) — просто выводит какой либо прямоугольный блок пикселей из любого места в RAM на видеовыход системы, допустимые битности цвета — 16 бит (1:5:5:5) и 32 бита (RGBA).
RDP (Reality Display Processor) — растеризатор от SGI. Растеризовал графические примитивы вполне в духе трёхмерных видеокарт своего времени в целочисленных координатах screenspace выполняя некий буфер команд. Для текстурирования у этого чипа было выделено 4Кб специализированный памяти под текстуры (TMEM), которые во многом были узким местом всей консоли — даже на фоне PS1 кадры из лучших игр N64 выглядят мыльновато.
RSP (Reality Signal Processor) — еще один базированный на MIPS R4200 процессор на 62МГц, уже без сопроцессора с плавающей запятой, зато с неким SIMD–сопроцессором опять таки для решения задач T&L (проецирования и вычисления параметров освещения вершин моделей на плоскость экрана) с регистрами с фиксированной точкой, формат и состав дополнительных команд которого, однако нигде я так и не смог нарыть толком.
RSP тоже обладал двумя собственными специализированными банками памяти — IMEM и DMEM — каждый опять же по 4Кб. В первом хранились инструкции для RSP, а во втором — данные. Что–то там было такое опять таки в сопроцессоре RSP, что это было два разных банка памяти.
Важно, что с основной RAM RSP мог общаться только посредством своего DMA–канала, всю обработку он должен был проводить внутри этих крошечных 2x4Кб памяти. Заниматься он должен был задачами T&L и обработки звука. Судя по всему — во втором качестве очень редко, т.к. с этим мог справится и ЦП.
А вот с графикой всё и так было впритык. Nintendo со старта консоли выпустила несколько «прошивок» (массивов «микрокодов»), которые ЦП должен был загружать в IMEM, чтобы использовать мощь RSP.
Первая из прошивок называлась Fast3D — это основная к использованию вещь, с большей частью и как правило точных функций. Но надо заметить, что координаты все тут были в screen–space и либо целочисленные, либо с фиксированной точкой после запятой. Так, например, ни координаты треугольника ни их разности по модулю не должны были превышать 32767, такое могло бы привести к глюкам обработки точки.
Вторая прошивка называлась Turbo3D — в ней была снижена точность обработки вершин и вырезаны многие фичи: обрезка по объёму видимости камеры, освещение, перспективная коррекция текстур, стек матриц.
Третий микрокод — Line3D умел почти всё то же что и Fast3D, но рисовал не треугольники, а линии для «проволочных» моделей.
Кроме того каждый микрокод был представлен еще в трёх вариантах того как он выдавал результирующие команды в растеризатор RDP. Первый — через собственный крохотный FIFO–буфер в собственной DMEM. Второй — через FIFO–буфер в RAM (имеет смысл размер от 1Кб). Третий вариант заливает все команды в один большой буфер в RAM не передавая его в RDP, чтобы это мог сделать ЦП по своему усмотрению.

Использовать RSP предполагалось по следующей схеме:
а) ЦП создаёт в памяти списки команд — это потенциально древовидные последовательности инструкций для RSP (до 10 уровней вложенности, т.к. одна из инструкций переходит к другому списку инструкций, а по его завершению возвращается обратно, стек возвратов при этом имеет глубину 10).
б) ЦП загружает в RSP микрокод под формат списка и передаёт указатель на первую инструкцию какого либо списка в RSP для начала обработки. В сущности списки команд содержат команды специфичные для того или иного микрокода — он их парсит и обрабатывает по заданному в IMEM алгоритму.
в) RSP начинает обрабатывать список команд — выполнять T&L, обрезку выпавших за камеру полигонов и т.п., формируя в итоге список команд для RDP и передаёт его собственно в RDP через порты ввода–вывода или DMA.
В силу ограниченности размеров IMEM/DMEM следует упомянуть как осуществлялась обработка и отрисовка вершин. Стандартные микрокоды размещали в DMEM массив из 16 вершин с рассчитаными атрибутами — команды в RSP рисующие треугольники содержали лишь индексы этих обработанных вершин (0–15). Загрузка в этот массив вершин осуществлялась отдельной командой загрузки из RAM вершин, которая тут же и рассчитывала трансформации им и освещение. Таким образом не могло идти подряд много команд отрисовки треугольников, если у всех них было больше 16 разных вершин — нужно было чередовать команды загрузки вершин и рисования треугольников порциями по не более чем 16 вершин между загрузками.

Таким образом в N64 мы можем говорить уже не только о полноценном, вынесенном из основного ЦП блока рассчёта T&L, но и о его изначальной полноценной программируемости — банк данных IMEM заполнялся в полной концептуальной аналогии с вершинными шейдерами.
Однако тут происходит нечто странное для меня — в первые годы после выхода консоли Nintendo никому не раскрывало формата команд RSP и его внутреннего устройства — предполагалось, что разработчики будут пользоваться только предложенными производителем банками IMEM распространяемыми с SDK в виде массивов байтов.
Ни инструментария, ни описания — ничего не было, поэтому разработчики так и поступали. При этом 4Кб под IMEM и 4Кб под DMEM действительно наступали на горло — так, например, банки Fast3D и Turbo3D не умели рисовать линии, отчего банк Line3D распространялся рядом с ними же.
Так что долгое время всё ограничивалось только тем, что Nintendo выпускало патчи к имеющемуся микрокоду и дополняло набор из трёх микрокодов какими то новыми — например был выпущен микрокод S2DEX для отрисовки двумерной графики/спрайтов.
И только под закат уже консоли Nintendo поспособствовала и несколько разработчиков внедрили свои коды в RSP, заточенные под их движки и форматы вершин — и это (по их словам) действительно сыграло в жирный плюс.

Списки команд подаваемые в RSP формировались в официальном API как правило как просто массив из макро–определений. Они были очень похожи на команды отдаваемые в RDP уже под растеризацию, собственно нередко просто повторяли их для простоты системы. Это были как правило 8–байтовые (64–битные) блоки байт, первый хранил код инструкции, а остальные — параметры (если они были, бывало и без параметров). Если параметров было много, блок мог увеличиваться с гранулярностью в те же 8 байт.
Команды RDP можно поделить на 3 категории — задание параметров рендера, собственно отрисовка и синхронизация. Судя по всему, для скорости RDP мог парсить и даже выполнять команды в буфере параллельно выполнению предыдущих команд отрисовки, поэтому иногда необходимо было делать синхронизацию.

Команды задания состояния

0x3f — установить фреймбуфер. адрес в памяти, ширина и цветность (8, 16 или 32 бита)
0x3d — установить адрес в памяти где лежит картинка для текстурных загрузок (битность 4/8/16/32, форматы цвета RGBA/YUV/Alpha/Monochrome)
0x3e — установить адрес в памяти где лежит Z–буфер (16 бит на пиксель)
0x2d — усановить окно обрезки (x0,y0,x1,y1) для всех команд отрисовки во фреймбуфере
0x2f — установить битовые стейты рендера. их очень много, самые важные:
— режим рендера (заливка/копирование/1–цикл/2–цикл)
— перспективная коррекция текстур
— параметры тестурирования типа мультитекстурирования, билинейной фильтрации или мипмэппинга
— вкл прозрачный цвет (croma key)
— дизеринг и тип дизеринга
— параметры смешивания цветов
— Z–тест и запись
— стенсиль и режим стенсиля (9–ые биты в ОЗУ, 2 бита стенсиля на 16–битные пиксели и 4 на 32–битные)
— брать Z из полигона или пикселя
0x35 — выставить параметры слота текстуры (где она находится в TMEM и какого она формата, слот tex от 0 до 7)
0x34 — загрузить текстуру из окна по (x0,y0,x1,y1) базовому адресу (см. 0x3d) в слот tex
0x37 — задать RGBA цвет для команды заливки прямоугольника

Команды отрисовки

0x36 — залить прямоугольник (x0,y0,x1,y1) (режим рендера должен быть «заливка»)
0x24 (128–битная команда!) — текстурированный прямоугольник (x0,y0,x1,y1),tex,(s,t,ds,dt) (режим рендера «копирование»)
0xXX: Здесь так же находятся команды отрисовки треугольников, но документ их не приводит.

Команды синхронизации

0x31 — подождать перед последующей загрузкой текстуры дорисовки вышележащих команд
0x28 — подождать перед последующим выставлением параметров текстурного слота дорисовки вышележащих команд
0x27 — полная синхронизация — ждёт пока не выполнятся все команды в очереди выше
0x29 — полная синхронизация с генерацией прерывания процессору

Забавным еще показалось отношение RDP к текстурированию — одним из глобальных стейтов RDP является «режим рендера». Режимы заливки и копирования предназначены для 2D–графики (при этом всё–равно в качестве источника битмапов выступает TMEM), а вот полноценные 3D–треугольники требуют уже режимов или 1–cycle или 2–cycle. В 1–cycle обрабатывается 1 тексель на пиксель — в качестве источника цвета может быть только одна текстурная выборка. В режиме 2–cycle же может производится до двух текстурных выборок на пиксель. Забавно, что билинейная фильтрация текстур тоже абсолютно попадает под это определение — выборка из двух слоёв разных мипмапов требует режима 2–cycle. Соответственно режим мультитекстурирования (точнее — битекстурирования) уже несовмпестим с мипмапами, структурно это в N64 одна и та же фича двойной текстурной выборки.

Операционная система

Много внимания в официальной документации Nintendo уделяется операционной системе — так что нельзя не сказать пару слов о ней.
Как они сами пишут «ОС получилась столь маленькая, что мы даже не дали ей названия». Вообще она распространяется не как обычная ОС, а как просто набор статических библиотек на C/C++ и полностью попадает на картридж.
Поэтому линкер вполне себе выкидывает неиспользуемые куски «оси» по факту компиляции. Ось даже не предоставляет менеджеров памяти — предполагается что игры работают в Kernel Mode и имеют прямой доступ в память.
Тем не менее ОС предполагает и большую ставку делает на вытесняющую многозадачность. При этом единственный механизм синхронизации — очереди сообщений, приписанные к каждому потоку в системе. Никаких семафоров или критических секций — потоки просто футболят друг другу сообщения, засыпая когда их не остаётся и просыпаясь, когда другой поток или ОС подкидывает новых.
Должен быть, к примеру, самый неприоритетный «idle thread», который дёргается когда перестают выполнятся любые другие потоки. Поэтому много вещей в API сделано в асинхронном стиле, документация прямо настаивает на том, чтобы приложение состояло из многих потоков, нагружающих друг друга сообщениями, чтобы возникало минимум простоев графического конвеера и тому подобное.
Вообще по официальной документации видно, что создатели консоли уже бережно ограждают разработчиков от необходимости лазить грязными пальцами в порты ввода–вывода — API внушительное, покрывает все аспекты и прикрывает аппаратную начинку достаточно плотно.

Итоги

Подводя итоги — возможно Nintendo 64 могла бы обогнать Playstation 1 по быстродействию и качеству картинки, если бы не родовые недостатки с форматом накопителя (максимум 64 Мб картриджа), зажатостью документации на RSP и всего 4Кб текстурной памяти. Это всё многое смазало, в прямом и переносном смыслах. Тем не менее в своём поколении консоль была вполне себе конкурентноспособна и подарила игрокам много замечательных игр.

1 комментарий

avatar
А вот об эту платформу я пообломал немало зубов, и так никуда и не продвинулся. У неё большой потенциал для homebrew — как раз выросло и ударилось ностальгию поколение её обладателей, но homebrew до сих пор нет: рынок со спросом и совершенно без конкуренции.

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

Ещё можно слить немного инсайдерской инфы и сказать, что были надежды получить лицензию на микрокод Factor 5, который даже лучше официального. Но уже не первый год воз и ныне там. Также есть проблема, как вообще что-то писать, даже если старый код будет получен — документации толком нет, всё заточено под древние SDK (дикая помесь gcc и Visual Studio), которые непонятно даже как просто установить, чтобы просто собрать написанные в них же исходники игр.
  • Shiru
  • +5
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.