helloGXM

Дисклеймер: Если ваша цель - создание 3D-графики для PSVita, почти наверняка вам нужно использовать игровые движки, умеющие создавать приложения для Vita. Другой общепринятый путь - использование обёртки VitaGL. Данный туториал создан только для облегчения процесса удовлетворения любопытства "А что это за GXM и как оно там", в ином случае (кроме удовлетворения инженерного зуда) знакомство с GXM почти наверняка не окупится.
Оглавление:
- N. Общая информация + software rendering + toolchain
-
[Search01] Инициализация libgxm
- [Search01_db] + регистрация смены буфера
- [Search02] Создание графического контекста
-
[Search03] Создание экранного буфера
- [Search03_db] Двойная буферизация
- [Search04] Создание сцены
- [Search05] Создание шейдер патчера
- [Search06] Создание буфера глубины.
- [Search07] Подготовка меша/вершинных данных.
- [Search08] Компиляция шейдеров
-
[Search09] Главный цикл, рендеринг сцен.
-
IX.a [Search9а] Инициализация сцены
- [Search9a_db] + DoubleBuffer
-
IX.б [Search9б] drawCall'ы
- [Search9б_db] + DoubleBuffer
-
IX.в [Search9в] Финализация сцены
- [Search9в_db] + DoubleBuffer
-
IX.г [Search9г] Регистрация буфера
- [Search9г_db] + DoubleBuffer
-
IX.a [Search9а] Инициализация сцены
- (SearX) Освобождение ресурсов
Разработка трёхмерных игр для PS Vita является нетривиальной задачей, так как Vita использует свой собственный, нигде более не используемый API 3D-графики, который нужно учить отдельно только для Vita. Так как коммерческий цикл жизни хендхелда закончен, а новые консоли сони используют другие API - изучение API libgxm становится очень спорным вопросом, с учётом сложности, малым объёмом обучающих материалов и нишевостью применения. Ещё раз: Вероятнее всего, если вы хотите просто делать 3D-приложения для виты, вам стоит использовать VitaGL - обёртку openGL над libgxm. Таким образом learning curve будет намного (намного!) более щадящей, результат, какой бы он ни был - будет получен намного быстрее и с большей вероятностью будет доведён до ума. Здесь же приводится туториал по работе именно с libgxm. Туториал служит цели снятию высокого порога знакомства с этой библиотекой в условиях малого количества доступной информации по libgxm.
Введение. Использование экрана PS Vita
Для вывода на экран достаточно получить у ядра блок памяти, использовать его как буфер изображения, и отправить этот буфер на дисплей (нативное разрешение дисплея PSVita 960x544, поддерживаются некоторые другие разрешения. Количество бит в строках выводимого изображения должно быть выровнено - кратно 64. Так как 960 4-битных пикселей кратно 64, использование значения 1024 не требуется)
Программы для Vita компилируются с помощью VitaSDK (опенсорсный, созданный коммьюнити тулчейн для Vita, разработанный поверх gcc). Официальный SDK Sony утёк в сеть, соответственно можно пользоваться и им, хотя к настоящему моменту VitaSDK настолько развит, что покрывает практически все вопросы разработки. Самая большая боль VitaSDK - отсутствие вменяемого отладчика. Отладчик Razor в SonySDK умеет в том числе очень продвинутую отладку GPU, для VitaSDK - отладочная печать и крашдампы, и медитация для GPU. Также в VitaSDK нет компилятора шейдеров, что составляет вторую большую проблему для графического программирования на Vita. Есть несколько путей обхода этой проблемы, хотя лучшим решением будет написание community шейдерного компилятора. Библиотеки и заголовочные файлы в VitaSDK и OfficialSDK слегка не совпадают, что абсолютно не является проблемой, если только не собирать один и тот же проект одновременно двумя тулчейнами.
Туториал написан для VitaSDK. Чтобы после установки SDK, для проверки тулчейна скомпилировать и запустить программу, нужно подключить заголовочный файл библиотеки. Объявления функций и типов для работы с дисплеем находятся (для VitaSDK), в psp2/display.h, с памятью - в psp2/kernel/sysmem.h, с графической библиотекой gxm - в psp2/gxm.h
Модуль библиотеки дисплея - libSceDisplay_stub.a, модули ядра компилятор линкует самостоятельно, потребующаяся в будущем gxm - libSceGxm_stub.a.
Соответственно порядок как и для компиляции любой другой программы для Виты:
$ arm-vita-eabi-gcc main.c -Wl,-q -c -o main.o
$ arm-vita-eabi-gcc main.o -Wl,-q -lSceDisplay_stub -lSceGxm_stub -o experiments.elf
$ vita-elf-create experiments.elf experiments.velf
$ vita-make-fself -s experiments.velf eboot.bin
$ vita-mksfoex -s TITLE_ID=DISPLAYBF "experiments" param.sfo
$ vita-pack-vpk -s param.sfo -b eboot.bin experiments.vpk
Компиляция должна пройти, а результирующий experiments.vpk после этого следует отправить на PSVita (к примеру по ftp, в VitaShell есть свой ftp-сервер). Там он должен успешно установиться (ныне для этого достаточно выбрать его в VitaShell), в результате появится иконка на LiveArea, и если по тапу на иконку экран заливается - тулчейн готов.
Выделение видеопамяти, выполняющееся в три приёма (расчёт размера с учётом кратности блока, выделение блока, получение указателя) потребуется далее не раз. В виде функции:
Ещё одно замечание по поводу выделения памяти:
Память должна быть размечена. Иногда требуются особенные варианты маппинга, но чаще всего это просто вызов sceGxmMapMemory() с нужными параметрами:
Особые варианты маппинга нужны при разметке памяти для шейдеров. Usse-блоки (универсальные шейдерные блоки) не умеют транслировать виртуальные адреса, им нужен адрес в физической памяти. Поэтому при разметке памяти для шейдерных блоков возвращается смещение в физической памяти. Поэтому же важно выделение памяти единым куском, а не страницами. В функции sceKernelAllocMemBlock это достигается (так сони обещает, не знаю насколько это правда) требованием куска с огромным выравниванием, по границе 1 мегабайта. Что мешает аллоку при этом вернуть кучу нарезанных страниц "одним" виртуальным куском, я не знаю)
Первый взгляд, или "libGXM - это просто и понятно"
Для отрисовки с использованием GPU PS Vita SGX543MP4+ (или, для краткости, SGX), среди прочих, могут быть использованы следующие возможности:
- Воспользоваться библиотекой libgxm, созданной Сони кaк часть API для работы с PS VITA. libgxm представляет высокоуровневую абстракцию для SGX. VitaSDK поддерживает libgxm почти в полном объеме, встречающиеся недоделки некритичны (Например в VitaSDK реализованы не все константы кодов ошибок. Т.е. сами коды ошибок будут возвращены (системными функциями SONY, VitaSDK использует их, а не подменяет), но удобно их проверить из кода пока не получится. Вот и возможность помочь сообществу, добавить и закоммитить);
- Воспользоваться библиотекой VitaGL - творение сцены, являющуюся обёрткой над libgxm, и реализующую API OpenGL 2.0, при этом реализованы также некоторые функции OpenGL3.3 (VitaGL так же, как и нативная библиотека, работает с Cg шейдерами. Но имеется экспериментальный конвертер из GLSL ES, так что можно писать на "почти" GLSL)
- PVR_PSP2. Альтернативная libgxm библиотека, сделанная на основе SDK для PowerVR - SGX DDK 1.8, но адаптированная к чипу с кастомизациями Сони. Лицензионая чистота сомнительна, по производительности - нужно тестировать. Непонятно.
Данный туториал рассматривает работу с libgxm. libgxm является более низкоуровневым API по сравнению c OpenGL. Если рассматривать OpenGL как API 3D-пайплайна, а Vulkan - как API видеокарты, то в таком подходе libgxm классифицируется ближе к Vulkan. VitaGL же ставит целью полностью повторить OpenGL, являясь обёрткой над GXM, не thin layer, и содержит множество "бутылочных горлышек" (Хотя создатель VitaGL на момент написания туториала активно поддерживает VitaGL и успешно улучшает и так прекрасную производительность VitaGL, "OpenGL compatibility profile" же реализован на 100%, если я не ошибаюсь)
В основном случае (за исключением нескольких экспериментальных конверторов) работа c графикой Vita подразумевает использование шейдерного языка Cg. В официальном SDK сони есть тулза для оффлайн компиляции, но в "народном", созданном community SDK её альтернативы нет. Есть сомнительной лицензионной чистоты файлик psp2cgc.exe (похоже просто взятый из официального SDK Sony), который (если позволяют ваши моральные принципы) можно юзать под линуксом под wine, к примеру обернув скриптом и интегрировав в ваш тулчейн.
Существует экспериментальный компилятор psp2spvc, но на момент написания туториала он находится в ранней стадии разработки и несколько лет заброшен (хотя, т.к. создавался он разработчиками активно развивающегося эмулятора VITA3K, есть надежда что о нём ещё вспомнят, и он не заброшен а поставлен на паузу). Также есть лицензионно чистый способ компиляции шейдеров в рантайме - с помощью модуля SceShaccCg (распотрошили "PlayStation Mobile Runtime Package", эдакий .NET от мира Sony, и достали из него рантайм-компилятор для Vita). При выборе рантайм-компиляции, для разработки проще всего пользоваться библиотекой vitaShaRK, a для автоматичекой установки SceShaccCg на vita - ShaRKFOOD. Минус этого способа что в таком случае ваша программа будет запускаться только на PSVita с установленным модулем. Потеряется переносимость. Я уверен что таким образом можно скомпилировать шейдер в рантайме на своей Vita, а потом просто сохранить получившийся бинарник и дальше пользоваться им, но ещё не пробовал так делать и не разобрался c юридической допустимостью такого подхода. Почему-то все, включая разработчика VitaGL заставляют конечных пользователей устанавливать ShaRKFOOD и не снимают эту головную боль с игроков. Это нарушало бы лицензию? Или разрабам лень просто файлик туда-сюда гонять? Или бинарь несовместим?
Приблизительная структура программы, работающей с SGX через libgxm:
В общем виде это всё, что нужно чтобы отрисовать треугольник. Далее разобран весь процесс по пунктам.
I (Search01) - Инициализация библиотеки.
Для инициализации библиотеки необходимо разрешить ей использовать память для внутреннего буфера, по умолчанию 16MB
В конце работы библиотека должна быть деинициализирована:
II (Search02) - Создание контекста.
Теперь, после того как библиотека инициализируется, необходим графический контекст. Можно рассматривать контекст как механические настройки видеокарты. В GXM контекст создаётся функциейint sceGxmCreateContext(const SceGxmContextParams *, SceGxmContext **)
(функция создаёт контекст непосредственного (immediate) режима, контекст режима удержания создаётся вызовом
sceGxmCreateDeferredContext()
)
Функция принимает параметры контекста и вернёт (через формальный параметр) указатель на созданый контекст. Объект контекста нельзя создать на стеке и его созданием занимается функция. Для внутренней работы SGX нужно выделить и предоставить создающей функции память, выделенную для:
- контекста
- vdm буфера
- вершинного буфера
- фрагментного буфера
- фрагментного usse-буфера.
У Vita'а есть видеопамять - CDRAM
, 112 MiB, и Main memory - USER_RW
, 365 MiB. USER_RW память кешируемая, но при неoбходимости вместо видеопамяти можно использовать для GPU USER_RW память, запросив её без кеширования. При достаточном количестве видеопамяти буфера выделяются в видеопамяти. Блоки видеопамяти должны быть выравнены по 256 KiB, при использовании USER_RW выравнивание по 4 KiB.) В данном туториале для упрощения вся память будет выделяться одинаково - в видеопамяти большими кусками.
При этом память для хранения самого контекста выделяется просто в куче.
III (Search03) - Дисплейный буфер.
Для создания дисплейного буфера нужны, в первую очередь, размеры дисплея. Здесь используется нативное разрешение дисплея.
Сам дисплейный буфер - участок в видеопамяти, в который SGX пишет отрендеренное изображение, и который потом выводится на экран. Но также нужен color surface - обёртка над сырым указателем на буфер.
Выделение памяти дисплейного буфера:
И создаётся surface-обёртка над буфером:
IV (Search04) - Создание рендер-таргета.
Рендер-таргет содержит в себе различные внутренние структуры, использующиеся для распараллеливания рендеринга изображения. Он описывает для SGX геометрические настройки рендера - исходя из размеров рендера и режима сглаживания будет настроен тайлинг, и организована дальнейшая внутренняя работа с этими тайлами.
V (Search05) - Создание шейдер патчера.
Шейдер-патчер нужен для патчинга (линковка плюс линковка вершинных и uniform данных) скомпилированных шейдеров. Для работы ему нужны выделенные буферы, плюс ещё необходимо предоставить коллбек-аллокатор памяти - в данном случае это просто обёртка над malloc():
VI (Search06) - Создание буфера глубины.
Даже если он не требуется, depthbuffer должен быть создан обязательно - этого требует внутренняя архитектура SGX, который по умолчанию осуществляет отложенный рендеринг, и соответственно использует буфер глубины для внутренней логики.
VII (Search07) - Подготовка геометрических данных.
Здесь всё просто - массив вершинных данных и массив индексов. В libgxm все отрисовки - индексные, поэтому если подразумевается отрисовка подряд, без массива индексов - можно создать один вспомогательный массив вида [0, 1, 2, 3, 4, ...] и пользоваться им для всех безиндекных мешей. Будет использована структура для хранения вершинных данныхПамять для вершинных данных и индексов выделяется в видеопамяти. SGX подразумевает размещение данных (и работу с ними со стороны CPU) непосредственно там, где они будут браться для отрисовки, чтобы избегать копирования каждый раз
В данном примере хак с выравниванием усложняет код, из-за использования топорной функции аллокации памяти. Так как видеопамять выделяется с выравниванием 256KB, размечена для рендера она должна быть тем же куском, что и выделена. Из-за этого здесь запрашивается сразу выровненный размер - чтобы тот же самый размер передать мапперу. Представленная ранее функция allocMapVideoMemory() мапит участок памяти с переданным размером, а не высчитанным выровненным размером.
VIII (Search08) - Компиляция шейдеров.
Как было отмечено в вводной части туториала, компиляция шейдеров представляет некоторую проблему для неофициальной разработки под PS Vita. В данном туториале бинари шейдера просто появляются из волшебной шляпы. Путь с vitaShaRK прямолинеен и прост, однако не является темой туториала.Сами шейдеры элементарны, вершинный просто pass-through, фрагментный и вовсе возвращает константу.
Выходной формат для COLOR0 (COLOR в данном случае) указан как UCHAR4 - как в экранном буфере. Без этого явного указания в буфер записывалось бы float-значение, как указано в шейдере. Для вывода на экран же подразумевается SCE_DISPLAY_PIXELFORMAT_A8B8G8R8.
IX (Search09) - Главный цикл, рендеринг сцен.
В общем виде здесь загружаются в очередь все сцены, и отправляются на рендеринг. Одна из сцен так же регистрируется для вывода на экран.Для одного треугольника же главный цикл ещё проще:
IX.a (Search9а) - Инициализация буфера команд для текущей поверхности.
IX.б (Search9б) - drawCall
IX.в (Search9в) - Финализация сцены.
IX.г (Search9г) - Отображение буфера.
Всё! осталось дождаться когда GPU завершит процесс и можно выводить результат на экран.Самый простой способ дождаться результата - остановить текущий поток до завершения работы над сценой
Управление вернётся потоку когда GPU закончит все команды из буфера команд и, соответственно, в экранном буфере будет готовое изображение. Теперь его можно вывести на экран так же, как это было сделано в самом-самом начале, ещё без участия SGX
Далее приводится вариант с двойной буферизацией
I._DB (Search01_db)
Смена буфера выносится в callback:
Указатель выносится в отдельную структуру, а не передаётся в callback непосредственно, т.к. для коллбека целиком копируются данные. Благодаря этому нет необходимости отслеживать время жизни передаваемых в коллбек данных, в callback будет передан указатель на копию. Но это значит, что если передавать указатель на буфер, каждый раз приходилось бы копировать весь буфер целиком, хотя в него на момент копирования даже не закончилась отрисовка, т.е. передавать указатель нет смысла - неинициализированная копия двух мегабайт на экране не нужна.
Теперь, чтобы обеспечить вызов коллбека, нужно зарегистрировать его при инициализации gxm (на первом шаге):
Структура типа SceGxmInitializeParams, кроме флагов (относящихся к мультипоточности) и разрешённого размера для своего внутреннего буфера, имеет 3 параметра, относящихся к дисплейному буферу:
unsigned int displayQueueMaxPendingCount
- глубина буфера
void (*displayQueueCallback)(const void *)
- коллбек-функция
size_t displayQueueCallbackDataSize
- размер данных, на которые ссылается параметр
display queue
- вводимая libgxm абстракция для обеспечения буферизации. Внутри библиотеки CPU посылает команды для GPU, и когда все команды для отрисовки посланы, эти команды помещаются в очередь. Когда GPU отрисует сцену, libgxm вызывает зарегистрированный коллбек для переключения экранного буфера.
При использовании дисплейной очереди, параметр displayQueueMaxPendingCount не может быть равен нулю и должен иметь значение не меньше 1. Для двойной буферизации этого значения достаточно - один буфер в очереди. Для тройной и более буферизации значение может быть 2 и более соответственно. В принципе это значение не зависит от буферизации, но обычно выставляется в (количество буферов - 1)
III.DB (Search03_db) Два буфера:
Чтобы поставить дисплейный буфер на ожидание в дисплейную очередь, нужны ещё и два примитива синхронизации типа SceGxmSyncObject. Они нужны gxm для того, чтобы синхронизировать операцию смены буфера с рендерингом. Примитивы синхронизации удобно создать одновременно с созданием дисплейного буфера. Так же теперь, для двойной буферизации, нужно два дисплейных буфера. Теперь шаг III выглядит так:
IX.a_DB (Search9a_db)
Для исполнения двойной буферизации в главном цикле будут поочерёдно заполняться буферы, чередуясь кто из них передний, кто задний. Для этого сцена инициализируется указателем на задний буфер, а так же при инициализации сцены ей передаётся указатель на объект синхронизации заднего буфера:
IX.б_DB (Search9б_db) Отрисовка - без изменений:
Здесь никаких изменений. Здесь то же самое заполнение очереди задач, от используемой буферизации не зависят команды для отрисовки примитивов.
IX.в_DB (Search9в_db) окончание сцены - без изменений:
Точно так же никаких изменений. Просто указание библиотеке, что больше команд на отрисовку для этой сцены не будет.
IX.г_DB (Search9г_db)
И вот теперь, вместо непосредственной отрисовки (после возврата управления из sceGxmFinish()), дисплейный буфер регистрируется в дисплейной очереди. В дисплейную очередь передаются также, вместе с адресом буфера, примитивы синхронизации текущего буфера и следующего.