Секреты и трюки старых консолей и компьютеров
GameDev.ru / Сообщества / old_tricks / Статьи / Графическая архитектура Playstation 1

Графическая архитектура Playstation 1

Автор:

Playstation 1 (она же - PSX)

Для начала вспомним историю. Появилась эта консоль в декабре 1994-го года. Другими словами всего через год после выхода псевдотрёхмерного Doom 1 и за полтора года до выхода Quake 1.
По данным с вики 3Dfx уже была основана как компания, но на рынок персоналок она войдёт позже - в 96-ом году. NVidia тоже уже была основана (в 93-ем), но первый видеоускоритель она выпустит только в 98-ом.
Таким образом PS1, обладавшая полноценным ускорителем трёхмерной полигональной графики фактически выступает в роли по меньшей мере одной из первых ласточек - возможно даже первой массовой машиной с аппаратным трёхмерным ускорением, хотя тут надо более аккуратно исследовать исторические реалии на предмет всяких 3DO и тому подобное.

Далее я очень кратко и сжато перескажу ключевые вещи из содержимого документа "Everything You Have Always Wanted to Know about the Playstation But Were Afraid to Ask" (http://hitmen.c02.at/files/docs/psx/psx.pdf), который подробно описывает архитектуру PS1 с точки зрения разработчика.

R3000A

В качестве ЦП в PS1 трудился MIPS R3000A - один из классических представителей RISC-архитектуры с частотой 33МГц. Регистровый банк - 32 целочисленных 32-битных регистра общего назначения повышенной ортогональности, плюс еще два 32-битных регистра в интегрированном модуле целочисленного деления/умножения. Ну и всякий прочий системный фарш. Единственное из любопытного что я могу вспомнить про архитектуру - это так называемый Delay Slot. Процессор появился еще без конвеера, но в качестве предтечи оного в нём всегда одна следующая инструкция извлекалась из памяти и дешифровалась в процессе выполнения текущей инструкции. Это правило было железобетонным и поэтому когда процессор встречал команду перехода (jump/branch), то сразу после перехода сперва выполнялась одна команда следующая за ним по старому адресу. Таким образом все команды переходов надо было "сдвинуть" на одну команду наверх, а если "сверху" команды не было - то добавить nop, чтобы всё вело себя без предсказуемо и без просадок. Это добавляло геморроя как в ассемблере, так и разработчикам компиляторов.

Под непосредственным управлением R3000A находилось 2Мб системной RAM и 2 сопроцессора.
Первый сопроцессор назывался Cop0 и по сути был менеджером виртуальной памяти и всяких утилитарных штук типа прерываний и реакций на ошибки.
Второй же сопроцессор назывался Cop2 и носил гордое название Graphics Transformation Engine (GTE).

GTE

Дело в том, что сопроцессора для вычислений с плавающей запятой в системе не было, но 3D-графика предполагала много всяких вычислений с вещественными числами. Поэтому вместо математического сопроцессора в R3000A впаяли сразу сопроцессор GTE - ровно то, что нужно было для 3D-графики - ни больше, ни меньше.
Это был классический сопроцессор - инструкции ему скармливал процессор, разбирая собственный поток инструкций и встречая специальный префикс. Выполняя свою инструкцию сопроцессор не мешал основному ЦП, то есть работал параллельно, но если основной ЦП пытался обратиться к нему в середине процесса - то он вынужден был остановится и подождать результат.
У GTE было 64 32-битных регистра - но все эти регистры были высокоспециализированными. Каждый из них хранил или возвращал вполне конкретные вещи для вполне конкретных вычислений нужных для графического пайплайна. Выражаясь современной терминологией GTE выполнял широкий набор довольно замысловатых инструкций выполняющих задачи T&L. Например у GTE была команда перемножающая точку на матрицу или команда обсчитывающая освещение точки от источника света. Параметры матрицы, к примеру, хранились в первых регистрах GTE - RxRy, выходные координаты точки сохранялись в регистрах MAC1/2/3, и так далее - регистров были десятки и все они были специфичны для тех или иных команд. Процессор мог загружать/сохранять данные в/из любого такого регистра, чем и обеспечивалось взаимодействие с системой.
Однако всё еще осложнялось (и ускорялось) тем, что все данные в GTE хранились с фиксированной точкой - причём в очень разных форматах. Например коэффициенты матриц хранились в формате 4:12 (знаковое 16-битное с двенадцатью битами после запятой), а вот коэффициенты трансляции в формате 20:12. Встречались еще форматы 16:16 и 28:4. В общем регистры GTE были воистину фаршем напополам с винегретом!

Поработав таким образом над исходными вершинами моделек, рассчитав им цвет и сконвертировав в целочисленные координаты screen-space-а процессор далее передавал данные в видеочип - в GPU.

GPU

Видеочип обладал собственной памятью VRAM в 1Мб в которую ЦП не имел доступа кроме как посредством плотного общения с GPU.

С GPU общение происходило через два 32-битных порта ввода-вывода отображенных в память ЦП.
Первый порт (Status/Control) при чтении возвращал самые основные статусные данные о состоянии видеочипа, например занят ли тот выполнением команды или свободен и ждёт новой. А при записи вменял одну из "глобальных команд" - например полный сброс видеочипа или установку видеорежима.
А вот второй порт (Data register) при записи помещал записываемое слово (в PS1 словом считается 32 бита) в FIFO-очередь команд. Видеочип ожидал от ЦП потока команд, которые по очереди выполнял. Команды были самые разные - от выставления параметров рендеринга (Render State) до отрисовки спрайта или треугольника. Все команды представляли из себя поток слов в заранее обозначенном формате - первое слово хранило идентификатор команды и самые важные параметры, далее следовали прочие данные, вплоть до огромных массивов при перекидывании битмапов. FIFO-очередь команд вмещала 16 слов (или 64 байта). Так как команды могли быть большими, то прежде чем записывать много данных туда надо было ждать опустошения очереди опрашивая регистр Status.
Чтобы процессору можно было не заниматься этой муторной работой один из семи DMA-каналов в системе был спроектирован специально для этого. В простом режиме он мог скормить в Data register большой банк данных, автоматически дожидаясь опустошения FIFO-очереди и подпитывая её по ходу дела. А вот в "сложном режиме" ему можно было скормить указатель на голову однонаправленного списка с данными в простом формате:
- Адрес следующего блока (FFFFFF для конца списка)
- Длина данных текущего блока в словах (может быть 0, тогда DMA-канал просто переходит к следующему блоку)
- Данные текущего блока (массив слов)
И таким образом "запитать" видеокарту на будущее большим и сложным набором команд к выполнению, который можно подготавливать долго и расчётливо.
В видеочипе не было Z-буфера и потому порядок отрисовки имел основополагающее значение - такое устройство очереди команд упрощало дело построения их в правильном порядке.

Вся VRAM в 1Мб с точки зрения GPU представлена как один большой битмап фреймбуфера 1024x512 16-битных пикселей. Забавно, что все данные поступающие для сохранения в VRAM никогда не указываются своими линейными адресами - всегда адресация идёт как бы к двум координатам (x,y) в этом фреймбуфере. Некоторое окно (границы которого программируются) в этом фреймбуфере отображается на дисплей (так что видеостраницы с vsync легко реализуются). Причём ему можно сменить битность с 16 бит на 24-битную (3 байта на пиксель). Но в этом режиме похоже не будет работать полупрозрачность и стенсиль (см. ниже), пояснений в документе нет.
Формат цветности - 1:5:5:5, где верхний бит трактуется по разному - смотря где он находится. Если это пиксель к записи во фреймбуфер, то это может быть бит полупрозрачности - тогда возможно на выбор 4 режима смешивания, со средним арифметическим в том числе. Если же это бит уже лежащий во фреймбуфере, то может быть включен режим Stencil check, запись пикселя будет отменяться, если бит зажжён. В режиме Stencil write любая запись пикселя во фреймуфер будет зажигать этот бит. В общем stencil как stencil.
В качестве текущей текстуры для текстурирования может быть выставлен блок из 256x256 пикселей (выбираемый с некоторой гранулярностью внутри фреймбуфера), причём внутри него с гранулярностью 8 пикселей могут быть выбраны "окна" для режимов с зацикливанием текстуры.
Сами текстуры могут быть в трёх цветовых форматах - 16, 8 и 4 бита на пиксель. Последние 2 требуют наличия Color Lookup Table (CLUT), т.к. байты хранят не сами цвета, а индексы в таблице из 16-битных цветов, причём таблицы эти так же представлены во фреймбуфере как бы просто полосками пикселей.

Перечислим вообще какие опции вообще возможны при рендеринге в качестве инструментов:
- полупрозрачность (однобитовая, по четырём возможным формулам, включая среднее арифметическое)
- текстурирование (правда совершенно без перспективной коррекции, что приводило к характерным искажениям)
- шейдинг для треугольников - как одноцветный так и по Гуро (вместе с текстурированием)
- автоматический дизеринг при рендере полигонов
- ограничение области рендера прямоугольным окном (хотя тут правильнее говорить о выборе произвольного прямоугольника во фреймбуфере как поверхности для рисования - то есть рендер в текстуру опять таки не вопрос и получается в этой системе с единым большим фреймбуфером автоматически)
- однобитный Stencil (в документации называется mask)

Итак, основная работа делалась с помощью списков команд.
Команд было много и разных - от отрисовки точек, спрайтов, полигонов (3 или 4 вершины), до закачки текстур или получения изображения в системную память обратно из видеокарты.
Например рассмотрим команду отрисовки монохромного треугольника, которая состоит из четырёх (32-битных) слов:
1) (байты) 0x20, B, G, R // 0x20 - код команды, BGR в 3-байтном формате
2) (полуслова) Y0, X0
3) (полуслова) Y1, X1
4) (полуслова) Y2, X2
Здесь видно, что координаты точек полигонов в видеочип передаются уже целочисленными в screen-space-е и по сути видеокарта занимается растеризацией без учета Z-координаты (и даже не имея Z-буфера).

Вот полный перечень команд с их кодами и кратким пояснением на английском:

Primitive drawing packets
 0x20     monochrome 3 point polygon
 0x24     textured 3 point polygon
 0x28     monochrome 4 point polygon
 0x2c     textured 4 point polygon
 0x30     gradated 3 point polygon
 0x34     gradated textured 3 point polygon
 0x38     gradated 4 point polygon
 0x3c     gradated textured 4 point polygon
 0x40     monochrome line
 0x48     monochrome polyline
 0x50     gradated line
 0x58     gradated line polyline
 0x60     rectangle
 0x64     sprite
 0x68     dot
 0x70     8*8 rectangle
 0x74     8*8 sprite
 0x78     16*16 rectangle
 0x7c     16*16 sprite
GPU command & Transfer packets
 0x01     clear cache
 0x02     frame buffer rectangle draw
 0x80     move image in frame buffer
 0xa0     send image to frame buffer
 0xc0     copy image from frame buffer
Draw mode/environment setting packets
 0xe1     draw mode setting
 0xe2     texture window setting
 0xe3     set drawing area top left
 0xe4     set drawing area bottom right
 0xe5     drawing offset
 0xe6     mask setting

Заключение

Таким образом видно, что архитектура PS1 уже по сути во многом предвосхищала будущее - задолго до эпохи аппаратного T&L в домашних видеокартах она делала это, правда средствами сопроцессора, плюс к тому концепция dedicated video memory с видеочипом как серверной сущностью, которой скармливается поток команд для параллельной работы с центральным процессором - всё это стояло сразу на шаг впереди glBegin/glEnd.

В общем впечатления остались довольно приятные - рассвет эпохи так сказать, первые ласточки, однако архитектура GPU в целом грамотная, хотя и предполагающая знание многих десятков и форматов и нюансов, но тем не менее это всё будет даже попроще пожалуй спецификации OpenGL 1.0. По крайней мере не сложнее. Биты все разложены логично и обоснованно.
Напомню еще, что первый релиз Direct3D случился двумя годами позже - в 96-ом. Хотя несомненно мысли вокруг аппаратных ускорений графики и API к ним бродили намного раньше - сама PS1 конечно появилась не на пустом месте.

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

Изображение

22 ноября 2017

#32bit, #5generation

2001—2017 © GameDev.ru — Разработка игр