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

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

Далее я очень кратко и сжато перескажу ключевые вещи из содержимого документа «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 конечно появилась не на пустом месте.

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

15 комментариев

avatar
Как пример, Haujobb — Strange Worlds.

avatar
Интересный момент про PS1 в том, что платформодержатель официально поддерживал homebrew. То есть уже в 1996-97 году обычному человеку с лишними деньгами можно было купить сцепконсоль со спец-SDK, и с помощью обычного 486 писать любительские программы для PS1. Некоторые удачные разработки потом публиковались на демо-дисках, прилагавшихся к журналу Official Playstation.
  • Shiru
  • +3
avatar
сегодня как раз sony анонсировали перевыпуск первой плейстейшн, вдвое меньше по габаритам и hdmi-видео.
avatar
Больше таких статей!
  • tsl
  • 0
avatar
Уже завтра будет еще одна. Про 3DO Interactive Multiplayer.
avatar
что характерно, при всей заточенности железа под 3D, GPU у PSX по своей сути чисто двумерный, и не умел выполнять попиксельную коррекцию перспективы, давая характерные искажения текстур под разными углами :) Интересно, насколько бы помогла, например, аппроксимация текстурных координат кривой второго\третьего порядка, как в некоторых software-рендерах — не нужно делать попиксельное деление (лишь два дополнительных сложения на пиксел), но и результат должен быть значительно лучше.

p.s. кстати, NVidia все-таки выпустила свой первый (и провальный, так как был заточен под quadratic texture maps а-ля Sega Saturn, в то время как все уже пересаживались на треугольники; кстати, тоже интересная история) ускоритель NV1 в 1995-м, а уже затем Riva 128, TNT, GeForce… и в конце концов пришли к RTX 2080Ti :)
avatar
Если мне не изменяет память, в Q1 на PC была линейная аппроксимация, там каждые N (16?) пикселей считалось точно, а между ними аффинная проекция. В результате текстуры почти не плыли, только если специально не искать такие моменты. На PS1 под закат пришли к подобным техникам, но грубее, как позволял GPU — разбивая крупные (ближние?) полигоны на множество мелких. Поэтому в поздних играх текстуры плывут значительно меньше.
avatar
Точно помню, что в Q1 и Q2 стены лабиринта никогда не искажались, а вот динамические объекты — монстры/аптечки плыли текстурами только в путь. Даже в OpenGL-рендере зачем то отключали на них коррекцию текстур.
avatar
«по своей сути чисто двумерный, и не умел выполнять попиксельную коррекцию перспективы»
Технически можно сказать, что до появления T&L в домашних видеокартах они тоже работали чисто с плоскостью и принимая на входе уже вычисленную в screen-space глубину Z для целей коррекции перспективы и z-test-ов являлись чисто растеризаторами.
avatar
Осенью 1995 года графика PS1 порвала мне мозг — то, что я видел на экране телевизора в супермаркете, было невозможно. До сих пор у меня валяется одна приставка, правда с убитым лазером. Временами даже хочется написать его эмулятор.

Процессор был еще без конвеера, но в качестве предтечи оного в нём всегда одна следующая инструкция извлекалась из памяти и дешифровалась в процессе выполнения текущей инструкции.
У финских VS1001 так же. После каждого JMP обязательно стоит NOP.
Вся VRAM в 1Мб с точки зрения GPU представлена как один большой битмап фреймбуфера 1024x512 16–битных пикселей. Забавно, что все данные поступающие для сохранения в VRAM никогда не указываются своими линейными адресами — всегда адресация идёт как бы к двум координатам (x,y) в этом фреймбуфере.
В тсконф у меня так лежат битмапы для спрайтового процессора.

Я правильно понимаю, что дисплей-лист выполняется блиттером (т.е. рисует 2-D битмапы в ОЗУ)? Для сравнения FT812 выполняет ДЛ каждую строку и строит 1-D пиксельный буфер «на лету». Но у FT812 частота повыше и транзисторов побольше на тот же функционал.

Хотелось бы узнать как устроен 3-D у Nintendo DS. Очень милая архитектура. Про него будет статья?
  • tsl
  • +1
avatar
«Я правильно понимаю, что дисплей-лист выполняется блиттером (т.е. рисует 2-D битмапы в ОЗУ)?»

Не понял вопроса. Видеочип PS1 может выполнять разные команды поступающие в порт ввода — от выставления render-state-ов и блиттинга 2Д-изображений до растеризации треугольников с текстурированием и освещением. Рисует он в свою видеопамять как в единый большой фреймбуфер (в котором можно выделить окно рендера).

Про Nintendo DS не планировал, но у меня будут еще обстоятельные уроки по программированию его непосредственного предтечи — Game Boy Advance, там даже обратная совместимость. Собственно они взяли GBA, навесили на него дополнительную память, расширили функционал видеочипа до 3D, вставили вдвое более быстрый процессор, два дисплея и получился DS.
avatar
Рисует он в свою видеопамять как в единый большой фреймбуфер (в котором можно выделить окно рендера).
Ну собственно, это и есть ответ.
Некоторые видеопроцы используют другой способ вывода видео. Каждую строку изображения дисплей лист выполняется полностью, но отрисовывается не фреймбуфер (битмап 1024х512, например), а буфер строки (1024х1), который обычно находится во внутренней памяти чипа. Такой способ требует намного меньше обращений к общему видеоОЗУ, поскольку оно используется только для чтения битмапов, текстур.
2Д:
— чтение текстуры,
— чтение фреймбуфера (+ накладывание по маске),
— запись фреймбуфера.
1Д:
— чтение текстуры (+ накладывание по маске на буфер строки, + запись в буфер строки).
Видеовыхлоп формируется из буфера строки (буфер, ессно двойной). Отрисовывается только часть сцены, видимая в данной строке.
Недостаток метода — меньший размер дисплей-листа, ибо за строку можно выполнить меньше команд. Профит — примерно в 3 раза меньший бандвиз ОЗУ.
avatar
Эм. Если я правильно понял такой подход используется как раз во всяких 8-битных и 16-битных консолях при этом «дисплей листом» в них можно считать таблицу описания спрайтов и параметры фонов. То есть прямо перед формированием очередного сканлайна видеоизображения видеочип просматривает таблицу спрайтов, выделяет те из них которые попадают в данный сканлайн и формируют эту строку — отсюда ограничения на максимальное количество спрайтов в одной строке и тому подобное. Главный плюс этого для 8/16-бит заключается в крайне скромным требованиям к количеству видеопамяти — таблица спрайтов очень экономно их описывает. Недостаток — списки спрайтов сканируются каждый сканлайн заново — и как следствие число обращений к видеопамяти резко увеличивается по сравнению с блиттинговым подходом — видеочип как правило не оставляет центральному процессору шансов обратиться к видеопамяти в периоды HDraw, а иногда и на весь VDraw.
avatar
В продвинутых чипах кроме спрайтов могут быть другие команды, например примитивы, заливки.
Количество же обращений к памяти как раз меньше, чем при блиттинге, и я описал почему (блиттер: чтение, чтение, запись, сканлайн: чтение).
avatar
«Некоторые видеопроцы используют другой способ вывода видео. Каждую строку изображения дисплей лист выполняется полностью, но отрисовывается не фреймбуфер (битмап 1024х512, например), а буфер строки (1024х1)»

Пишу сейчас про Atari Jaguar (а это ровесница Playstation 1) — там как раз по сути синтез таких подходов.
На видеовыход подаётся информация именно из строки-буфера в то время как в теневую такую же рисуются пиксели для следующей строки. При этом видеоадаптер для построения следующей строки проходит по командам дисплейного списка, где типовыми объектами являются битмапы из которых один сканлайн за раз переносится со всевозможными наложениями и прозрачностями диктуемыми порядком отрисовки. Таким образом это могут быть как спрайты из атласа так и почти полноэкранными битмапами в которые рендерил специализированный процессор. Черновик статьи с этим можно сейчас посмотреть тут: gamedev.ru/flame/forum/?id=231027&page=7&m=4976559#m95
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.