helloGXM

Отрисовка треугольника на PS Vita c использованием 3D-ускорителя

Дисклеймер: Если ваша цель - создание 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
  • (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 не требуется)

main.c
#include <psp2/display.h> #include <psp2/kernel/sysmem.h> int main() { void *display_buffer_data; /* Video memory allocation */ /* Calculate buffer size and buffer memblock size */ /* (latter should be a multiple of 4KB for main memory, 256KB for GPU) */ uint32_t size_unaligned = 4 * 960 * 544; uint32_t size_aligned = size_unaligned - (size_unaligned % (256 * 1024)) + 256 * 1024; /* Allocate memory block */ SceUID display_buffer_uid = sceKernelAllocMemBlock( "display_buffer" , SCE_KERNEL_MEMBLOCK_TYPE_USER_CDRAM_RW , size_aligned , NULL); /* Get pointer to allocated memory block */ sceKernelGetMemBlockBase(display_buffer_uid , &display_buffer_data); /* wrote buffer to some pretty debug color */ /* (A8B8G8R8 pixel format will be proposed to display later) */ /* (as the display_fb.pixelformat = SCE_DISPLAY_PIXELFORMAT_A8B8G8R8) */ uint32_t *pixel = (uint32_t*) display_buffer_data; for (uint32_t i = 0; i < 960 * 544; i++) { pixel[i] = 0xffffff00; } /* wrap void* buffer to sony display wrapper */ SceDisplayFrameBuf display_fb; display_fb.size = sizeof(SceDisplayFrameBuf); display_fb.base = display_buffer_data; display_fb.pitch = 960; display_fb.pixelformat = SCE_DISPLAY_PIXELFORMAT_A8B8G8R8; display_fb.width = 960; display_fb.height = 544; sceDisplaySetFrameBuf(&display_fb, SCE_DISPLAY_SETBUF_NEXTFRAME); for(;;); sceKernelFreeMemBlock(display_buffer_uid); return 0; }

Программы для 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, и если по тапу на иконку экран заливается - тулчейн готов.

Выделение видеопамяти, выполняющееся в три приёма (расчёт размера с учётом кратности блока, выделение блока, получение указателя) потребуется далее не раз. В виде функции:

memory allocation
void *allocVideoMemory(uint32_t size) { /* big chunks of CDRAM memory, RW access hardcoded here */ /* that's not what should be used at all cases */ void *buffer; size = size - (size % (256 * 1024)) + 256 * 1024; SceUID uid = sceKernelAllocMemBlock( "video_memory" , SCE_KERNEL_MEMBLOCK_TYPE_USER_CDRAM_RW , size , NULL); sceKernelGetMemBlockBase(uid, &buffer); return buffer; } void deallocVideoMemory(void *buffer) { SceUID uid = sceKernelFindMemBlockByAddr(buffer, 0); sceGxmUnmapMemory(buffer); sceKernelFreeMemBlock(uid); }

Ещё одно замечание по поводу выделения памяти:
Память должна быть размечена. Иногда требуются особенные варианты маппинга, но чаще всего это просто вызов sceGxmMapMemory() с нужными параметрами:

memory allocation
void *allocMapVideoMemory(SceSize size) { void *buffer = allocVideoMemory(size); sceGxmMapMemory(buffer, size, SCE_GXM_MEMORY_ATTRIB_RW); return buffer; }

Особые варианты маппинга нужны при разметке памяти для шейдеров. 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:

main.pseudo
0. (...официальные разработчики и ушлые личности, ещё при этом и владеющие девкитами, первым делом инициализируют Razor и наслаждаются дебаггером/GPU дебаггером...) // 1. (Search01) Инициализация библиотеки SceGxmInitializeParams gxm_init_params; gxm_init_params.flags = 0; gxm_init_params.displayQueueMaxPendingCount = 1; gxm_init_params.displayQueueCallback = NULL; gxm_init_params.displayQueueCallbackDataSize = 0; gxm_init_params.parameterBufferSize = SCE_GXM_DEFAULT_PARAMETER_BUFFER_SIZE; sceGxmInitialize(&gxm_init_params); (Здесь регистрируется коллбек, который будет вызываться при окончании рендеринга всей сцены. В коллбеке нужно послать буфер на отрисовку) { sceGxmInitializeParams.displayQueueCallback = [](void *framebuffer){ sceDisplaySetFrameBuf(framebuffer) } } sceGxmInitialize(SceGxmInitializeParams) // 2. (Search02) Создание контекста (Выделение памяти для внутренних буферов // SGX для обработки всей геометрии) { SceGxmContextParams.context_memory = malloc() SceGxmContextParams.vdm_buffer = malloc() SceGxmContextParams.vertex_buffer = malloc() SceGxmContextParams.fragment_buffer = malloc() SceGxmContextParams.fragment_usse_buffer = malloc() } SceGxmContext = sceGxmCreateContext(SceGxmContextParams) // 3. (Search03) // Создание буферов экрана и, заодно, синхронизатора каждого буфера for(BUFFER_COUNT): render_buffer[i] = malloc() SceGxmColorSurface[i] = sceGxmColorSurfaceInit(render_buffer[i]) SceGxmSyncObject[i] = sceGxmSyncObjectCreate() // 4. (Search04) создание сцены и выделение памяти для хранения этих параметров { SceGxmRenderTargetParams.driver_block = malloc() } SceGxmRenderTarget = sceGxmCreateRenderTarget(SceGxmRenderTargetParams) (рендер таргет описивает только геометрические параметры задачи отрисовки. Размер изображения, разбиение его на тайлы. Параметрами парралелизации задачи и очередностью конкретных drawcall'ов сцены занимается пара sceGxmBeginScene()/sceGxmEndScene() далее) // 5. (Search05) Создание шейдер патчера (Выделение памяти для внутренних буферов патчера). { SceGxmShaderPatcherParams.bufferMem = malloc() SceGxmShaderPatcherParams.vertexUsseMem = malloc() SceGxmShaderPatcherParams.fragmentUsseMem = malloc() } SceGxmShaderPatcher = sceGxmShaderPatcherCreate(SceGxmShaderPatcherParams) // 6. (Search06) Создание буфера глубины. Даже если он не требуется явно, он должен быть создан обязательно - этого требует внутренняя архитектура SGX, который по умолчанию осуществляет отложенный рендеринг, и соответственно использует буфер глубины для внутренней логики. { DepthBuffer = malloc() } SceGxmDepthStencilSurface = sceGxmDepthStencilSurfaceInit(DepthBuffer) // 7. (Search07) Подготовка геометрических данных. struct Vertice meshVertexData[] = malloc() short meshIndices[] = malloc() for(i in VertexCount): meshVertexData[i] = {init Vertice} for(j in indexCount): meshIndices[j] = {init index} // 8. (Search08) Компиляция шейдеров (официальным разработчикам здесь остаётся только пропатчить готовые шейдера, "патчинг" - это линковка на стероидах) { SceGxmProgram = FILE.load("vertex.gxp"); } SceGxmVertexProgram = sceGxmShaderPatcherCreateVertexProgram(SceGxmProgram) { SceGxmProgram = FILE.load("fragment.gxp"); } SceGxmFragmentProgram = sceGxmShaderPatcherCreateFragmentProgram(SceGxmProgram) // 9. (Search09) Главный цикл, рендеринг сцен. for(;;): for(scene in scenes): // 9.a (Search9а) // Инициализация буфера команд для текущей поверхности sceGxmBeginScene(SceGxmContext, SceGxmRenderTarget, SceGxmSyncObject[back], SceGxmColorSurface[back]) // 9.б (Search9б) Загрузка drawCall'ов в очередь: for(drawCall in drawCalls[]): // Загружаем буффер команд ...optional: SetTextures() ...optional: SetUniforms() sceGxmSetVertexProgram(SceGxmContext , SceGxmVertexProgram) sceGxmSetFragmentProgram(SceGxmContext , SceGxmFragmentProgram) sceGxmSetVertexStream(SceGxmContext , meshVertexData) sceGxmDraw(context, meshIndices) // 9.в (Search9в) // Буфер команд загружен, больше в данной сцене команд не будет: sceGxmEndScene(SceGxmContext) // 9.г (Search9г) Чтобы SGX вызвал коллбек, меняющий дисплейный буфер на буфер с готовым изображением, в дисплейную очередь должен быть помещён объект синхронизации, проассоциированный с этим буфером: sceGxmDisplayQueueAddEntry( SceGxmSyncObject[frontBuffer] SceGxmSyncObject[backBuffer] SceGxmColorSurface[backBuffer]) // (Всё! Готово. Теперь, когда SGX отрисует сцену (ту, что предназначена для вывода на экран), будет вызван коллбек зарегистрированный ранее. В нём на экран выводится получившееся изображение. // 10. (SearX) Освобождение ресурсов, завершение работы for(memory in allAllocatedMemory): memory.free() for(resource in usedResource): sceDestroyResource(resource) sceGxmDestroyContext() sceGxmTerminate()

В общем виде это всё, что нужно чтобы отрисовать треугольник. Далее разобран весь процесс по пунктам.

I (Search01) - Инициализация библиотеки.
#include <psp2/gxm.h>
Для инициализации библиотеки используется функция int sceGxmInitialize(const SceGxmInitializeParams *params). Формальный параметр должен указывать на настройки, с которыми gxm должна быть инициализирована.

Для инициализации библиотеки необходимо разрешить ей использовать память для внутреннего буфера, по умолчанию 16MB

GXM initialization
SceGxmInitializeParams gxm_init_params; gxm_init_params.flags = 0; gxm_init_params.displayQueueMaxPendingCount = 0; gxm_init_params.displayQueueCallback = NULL; gxm_init_params.displayQueueCallbackDataSize = 0; gxm_init_params.parameterBufferSize = SCE_GXM_DEFAULT_PARAMETER_BUFFER_SIZE; if(sceGxmInitialize(&gxm_init_params) != SCE_OK) { /* GXM initialization error. Already initialized or wrong params */ return 1; }
(Здесь приведена проверка возвращаемого значения функции, но далее проверка будет пропускаться для упрощения текста туториала. Но за неимением отладчика, проверка возвращаемого значения - святое дело, не стоит забывать его в своём коде хотя бы ради облегчения разработки)

В конце работы библиотека должна быть деинициализирована:

sceGxmTerminate();

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.) В данном туториале для упрощения вся память будет выделяться одинаково - в видеопамяти большими кусками.

При этом память для хранения самого контекста выделяется просто в куче.

Context
SceGxmContext *context; /* Context pointer to be used in following steps */ SceSize size; void *buffer; SceGxmContextParams context_param; context_param.hostMem = malloc(SCE_GXM_MINIMUM_CONTEXT_HOST_MEM_SIZE); context_param.hostMemSize = SCE_GXM_MINIMUM_CONTEXT_HOST_MEM_SIZE; /* VDM_RingBuffer */ size = SCE_GXM_DEFAULT_VDM_RING_BUFFER_SIZE; buffer = allocMapVideoMemory(size); context_param.vdmRingBufferMem = buffer; context_param.vdmRingBufferMemSize = size; /* Vertex_RingBuffer */ size = SCE_GXM_DEFAULT_VERTEX_RING_BUFFER_SIZE; buffer = allocMapVideoMemory(size); context_param.vertexRingBufferMem = buffer; context_param.vertexRingBufferMemSize = size; /* FragmentRingBuffer */ size = SCE_GXM_DEFAULT_FRAGMENT_RING_BUFFER_SIZE; buffer = allocMapVideoMemory(size); context_param.fragmentRingBufferMem = buffer; context_param.fragmentRingBufferMemSize = size;
Фрагментному Usse буферу важен дополнительный параметр - смещение. Поэтому маппинг осуществляется не выделяющей память функцией, а отдельной функцией для маппинга фрагментного usse буфера
Context (Fragment USSE exception)
/* FragmentUsseRingBuffer */ size = SCE_GXM_DEFAULT_FRAGMENT_USSE_RING_BUFFER_SIZE; buffer = allocVideoMemory(size); context_param.fragmentUsseRingBufferMem = buffer; context_param.fragmentUsseRingBufferMemSize = size; sceGxmMapFragmentUsseMemory( buffer , size , &context_param.fragmentUsseRingBufferOffset); /* Context creation */ sceGxmCreateContext(&context_param, &context);
Уничтожается контекст вызовом
sceGxmDestroyContext(context);

III (Search03) - Дисплейный буфер.

Для создания дисплейного буфера нужны, в первую очередь, размеры дисплея. Здесь используется нативное разрешение дисплея.

main.c
#define DISPLAY_WIDTH 960 #define DISPLAY_HEIGHT 544

Сам дисплейный буфер - участок в видеопамяти, в который SGX пишет отрендеренное изображение, и который потом выводится на экран. Но также нужен color surface - обёртка над сырым указателем на буфер.

main.c
void *display_buffer; SceGxmColorSurface display_surface;

Выделение памяти дисплейного буфера:

main.c
uint32_t buff_size = 4 * DISPLAY_WIDTH * DISPLAY_HEIGHT; display_buffer = allocMapVideoMemory(buff_size);
Здесь буфер будет заполнен дебаг-цветом. Но логика работы с libgxm очистки буфера не подразумевается, вместо очистки экрана используется фоновый (служебный) полигон закрашиваемый служебным же шейдером, здесь данный механизм не рассматривается. (Ещё раз отметим, что заполнение буфера цветом - это хак)
main.c
#define DEBUG_COLOR 0xffffcc33 uint32_t *pixel = (uint32_t*)display_buffer; for (uint32_t k = 0; k < buff_size / 4; k++) { pixel[k] = DEBUG_COLOR; }

И создаётся surface-обёртка над буфером:

main.c
sceGxmColorSurfaceInit( &display_surface , SCE_GXM_COLOR_FORMAT_A8B8G8R8 , SCE_GXM_COLOR_SURFACE_LINEAR , SCE_GXM_COLOR_SURFACE_SCALE_NONE , SCE_GXM_OUTPUT_REGISTER_SIZE_32BIT , DISPLAY_WIDTH , DISPLAY_HEIGHT , DISPLAY_WIDTH , display_buffer);

IV (Search04) - Создание рендер-таргета.
Рендер-таргет содержит в себе различные внутренние структуры, использующиеся для распараллеливания рендеринга изображения. Он описывает для SGX геометрические настройки рендера - исходя из размеров рендера и режима сглаживания будет настроен тайлинг, и организована дальнейшая внутренняя работа с этими тайлами.
main.c
SceGxmRenderTarget *render_target; SceGxmRenderTargetParams render_target_params; render_target_params.flags = 0; render_target_params.width = DISPLAY_WIDTH; render_target_params.height = DISPLAY_HEIGHT; render_target_params.scenesPerFrame = 1; render_target_params.multisampleMode = SCE_GXM_MULTISAMPLE_NONE; render_target_params.multisampleLocations = 0; render_target_params.driverMemBlock = -1; sceGxmCreateRenderTarget(&render_target_params , &render_target);

V (Search05) - Создание шейдер патчера.
Шейдер-патчер нужен для патчинга (линковка плюс линковка вершинных и uniform данных) скомпилированных шейдеров. Для работы ему нужны выделенные буферы, плюс ещё необходимо предоставить коллбек-аллокатор памяти - в данном случае это просто обёртка над malloc():
main.c
static void *patcherHostAlloc(void *userData, uint32_t size) { (void)userData; return malloc(size); } static void patcherHostFree(void *userData, void *mem) { (void)userData; free(mem); } ... SceGxmShaderPatcher *shader_patcher; SceGxmShaderPatcherParams shader_patcher_params; SceSize buffer_size = 64*1024; unsigned int offset; shader_patcher_params.userData = NULL; shader_patcher_params.hostAllocCallback = patcherHostAlloc; shader_patcher_params.hostFreeCallback = patcherHostFree; buffer = allocMapVideoMemory(buffer_size); shader_patcher_params.bufferAllocCallback = NULL; shader_patcher_params.bufferFreeCallback = NULL; shader_patcher_params.bufferMem = buffer; shader_patcher_params.bufferMemSize = buffer_size; buffer = allocVideoMemory(buffer_size); sceGxmMapVertexUsseMemory(buffer , buffer_size , &offset); shader_patcher_params.vertexUsseAllocCallback = NULL; shader_patcher_params.vertexUsseFreeCallback = NULL; shader_patcher_params.vertexUsseMem = buffer; shader_patcher_params.vertexUsseMemSize = buffer_size; shader_patcher_params.vertexUsseOffset = offset; buffer = allocVideoMemory(buffer_size); sceGxmMapFragmentUsseMemory(buffer , buffer_size , &offset); shader_patcher_params.fragmentUsseAllocCallback = NULL; shader_patcher_params.fragmentUsseFreeCallback = NULL; shader_patcher_params.fragmentUsseMem = buffer; shader_patcher_params.fragmentUsseMemSize = buffer_size; shader_patcher_params.fragmentUsseOffset = offset; sceGxmShaderPatcherCreate(&shader_patcher_params , &shader_patcher);

VI (Search06) - Создание буфера глубины.
Даже если он не требуется, depthbuffer должен быть создан обязательно - этого требует внутренняя архитектура SGX, который по умолчанию осуществляет отложенный рендеринг, и соответственно использует буфер глубины для внутренней логики.
main.c
SceGxmDepthStencilSurface surface_depth; void *depth_buffer = allocMapVideoMemory(DISPLAY_WIDTH * DISPLAY_HEIGHT * 4); sceGxmDepthStencilSurfaceInit(&surface_depth , SCE_GXM_DEPTH_STENCIL_FORMAT_S8D24 , SCE_GXM_DEPTH_STENCIL_SURFACE_TILED , DISPLAY_WIDTH , depth_buffer , NULL);

VII (Search07) - Подготовка геометрических данных.
Здесь всё просто - массив вершинных данных и массив индексов. В libgxm все отрисовки - индексные, поэтому если подразумевается отрисовка подряд, без массива индексов - можно создать один вспомогательный массив вида [0, 1, 2, 3, 4, ...] и пользоваться им для всех безиндекных мешей. Будет использована структура для хранения вершинных данных
main.c
struct vertex { float x, y, z; };
Вершинные данные будут размечены на этапе патчинга шейдера.

Память для вершинных данных и индексов выделяется в видеопамяти. SGX подразумевает размещение данных (и работу с ними со стороны CPU) непосредственно там, где они будут браться для отрисовки, чтобы избегать копирования каждый раз

main.c
struct vertex *mesh_data; unsigned short *indices_data; SceSize size_min_block = 256 * 1024; //assert(3 * sizeof(struct vertex) < size_min_block) mesh_data = allocMapVideoMemory(size_min_block); mesh_data[0] = (struct vertex){0.0f, 0.75f, 0.5f}; mesh_data[1] = (struct vertex){-0.75f, -0.75f, 0.5f}; mesh_data[2] = (struct vertex){0.75f, -0.75f, 0.5f}; indices_data = allocMapVideoMemory(size_min_block); indices_data[0] = 0; indices_data[1] = 1; indices_data[2] = 2;

В данном примере хак с выравниванием усложняет код, из-за использования топорной функции аллокации памяти. Так как видеопамять выделяется с выравниванием 256KB, размечена для рендера она должна быть тем же куском, что и выделена. Из-за этого здесь запрашивается сразу выровненный размер - чтобы тот же самый размер передать мапперу. Представленная ранее функция allocMapVideoMemory() мапит участок памяти с переданным размером, а не высчитанным выровненным размером.

VIII (Search08) - Компиляция шейдеров.
Как было отмечено в вводной части туториала, компиляция шейдеров представляет некоторую проблему для неофициальной разработки под PS Vita. В данном туториале бинари шейдера просто появляются из волшебной шляпы. Путь с vitaShaRK прямолинеен и прост, однако не является темой туториала.

Сами шейдеры элементарны, вершинный просто pass-through, фрагментный и вовсе возвращает константу.

vertex.cg
float4 main(float3 position) : POSITION { return float4(position, 1.0f); }
Vertex Inlined Binary
const char vertex_gxp[] = { 0x47, 0x58, 0x50, 0x00, 0x01, 0x05, 0x50, 0x03, 0xf9, 0x00, 0x00, 0x00, 0x98, 0xaf, 0xcf, 0xfb, 0x9f, 0xd8, 0xac, 0x83, 0x04, 0x00, 0x19, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xb8, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x04, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x74, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x40, 0x3e, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x44, 0xfa, 0x01, 0x00, 0x04, 0x90, 0x85, 0x11, 0xa5, 0x08, 0x41, 0x00, 0x54, 0x90, 0x89, 0x11, 0xc1, 0x08, 0x00, 0x00, 0x20, 0xa0, 0x00, 0x50, 0x27, 0xfb, 0x10, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00, 0x00 };
fragment.cg
float4 main() : COLOR { return float4(1.0f, 0.9f, 0.8f, 1.0f); }
Fragment Inlined Binary
const char fragment_gxp[] = { 0x47, 0x58, 0x50, 0x00, 0x01, 0x05, 0x50, 0x03, 0xdc, 0x00, 0x00, 0x00, 0x21, 0x99, 0x77, 0xda, 0x05, 0x6a, 0xf9, 0x28, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb4, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x02, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x68, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6c, 0x00, 0x00, 0x00, 0x40, 0x3e, 0x03, 0x00, 0x02, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x44, 0xfa, 0x00, 0x00, 0x00, 0xc5, 0x22, 0x04, 0x80, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3c, 0x33, 0x3b, 0x01, 0x00, 0x00, 0x00, 0x66, 0x3a, 0x00, 0x3c, 0x13, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00 };
main.c
... SceGxmVertexProgram *vertex_program_patched = NULL; SceGxmShaderPatcherId vertex_program_id; SceGxmVertexAttribute vertex_attribute; vertex_attribute.streamIndex = 0; vertex_attribute.offset = 0; vertex_attribute.format = SCE_GXM_ATTRIBUTE_FORMAT_F32; vertex_attribute.componentCount = 3; vertex_attribute.regIndex = sceGxmProgramParameterGetResourceIndex( sceGxmProgramFindParameterByName( (SceGxmProgram*)vertex_gxp, "position" ) ); sceGxmShaderPatcherRegisterProgram(shader_patcher , (SceGxmProgram*)vertex_gxp , &vertex_program_id); SceGxmVertexStream vertex_stream; vertex_stream.stride = sizeof(struct vertex); vertex_stream.indexSource = SCE_GXM_INDEX_SOURCE_INDEX_16BIT; sceGxmShaderPatcherCreateVertexProgram(shader_patcher , vertex_program_id , &vertex_attribute , 1 , &vertex_stream , 1 , &vertex_program_patched);
На этом этапе размечаются вершинные атрибуты, и полученный шейдер будет брать их так, как они сейчас описаны.

main.c
/* Fragment Shader */ SceGxmFragmentProgram *fragment_program_patched = NULL; SceGxmShaderPatcherId fragment_program_id; sceGxmShaderPatcherRegisterProgram(shader_patcher , (SceGxmProgram*)fragment_gxp , &fragment_program_id); sceGxmShaderPatcherCreateFragmentProgram(shader_patcher , fragment_program_id , SCE_GXM_OUTPUT_REGISTER_FORMAT_UCHAR4 , SCE_GXM_MULTISAMPLE_NONE , NULL , NULL , &fragment_program_patched);
В общем случае фрагментный шейдер должен получать вершинный шейдер, но в текущем примере для него нет входных параметров из вершинного шейдера, поэтому здесь это не обязательно.

Выходной формат для COLOR0 (COLOR в данном случае) указан как UCHAR4 - как в экранном буфере. Без этого явного указания в буфер записывалось бы float-значение, как указано в шейдере. Для вывода на экран же подразумевается SCE_DISPLAY_PIXELFORMAT_A8B8G8R8.

IX (Search09) - Главный цикл, рендеринг сцен.
В общем виде здесь загружаются в очередь все сцены, и отправляются на рендеринг. Одна из сцен так же регистрируется для вывода на экран.
Main loop
for(;;): for(scene in scenes): IX.a for(in scene): IX.б IX.в IX.г

Для одного треугольника же главный цикл ещё проще:

Simple main loop
for(;;) { IX.a IX.б IX.в IX.г }

IX.a (Search9а) - Инициализация буфера команд для текущей поверхности.

main.c
sceGxmBeginScene(context , 0 , render_target , NULL , NULL , NULL , &display_surface , &surface_depth);
Даётся указание SGX начать работу со сценой. Все дальнейшие команды этого контекста будут рендериться с настройками render target'а и писаться в указанный буфер

IX.б (Search9б) - drawCall
main.c
sceGxmSetVertexProgram(context, vertex_program_patched); sceGxmSetFragmentProgram(context, fragment_program_patched); sceGxmSetVertexStream(context, 0, mesh_data); sceGxmDraw(context , SCE_GXM_PRIMITIVE_TRIANGLES , SCE_GXM_INDEX_FORMAT_U16 , indices_data , 3);
Совсем очевидно: устанавливаем шейдера, вершинный стрим меша, и вызывается sceGxmDraw с индексами меша - команда на отрисовку помещается в буфер команд GPU

IX.в (Search9в) - Финализация сцены.
main.c
sceGxmEndScene( context , NULL , NULL);
Закрывается сцена, GPU теперь знает что больше команд не поступит и работа над неизменяемой сценой не будет нарушена новыми командами.

IX.г (Search9г) - Отображение буфера.
Всё! осталось дождаться когда GPU завершит процесс и можно выводить результат на экран.

Самый простой способ дождаться результата - остановить текущий поток до завершения работы над сценой

sceGxmFinish(context);

Управление вернётся потоку когда GPU закончит все команды из буфера команд и, соответственно, в экранном буфере будет готовое изображение. Теперь его можно вывести на экран так же, как это было сделано в самом-самом начале, ещё без участия SGX

main.c
SceDisplayFrameBuf display_fb; display_fb.size = sizeof(display_fb); display_fb.base = display_buffer; display_fb.pitch = DISPLAY_WIDTH; display_fb.pixelformat = SCE_DISPLAY_PIXELFORMAT_A8B8G8R8; display_fb.width = DISPLAY_WIDTH; display_fb.height = DISPLAY_HEIGHT; sceDisplaySetFrameBuf(&display_fb, SCE_DISPLAY_SETBUF_NEXTFRAME);
Его величество Треугольник собственной персоной!

Далее приводится вариант с двойной буферизацией

I._DB (Search01_db)

Смена буфера выносится в callback:

main.c
struct DisplayCallbackData { void *addr; }; void display_queue_callback(const void *callbackData) { SceDisplayFrameBuf display_fb; const struct DisplayCallbackData *cb_data = callbackData; display_fb.size = sizeof(display_fb); display_fb.base = cb_data->addr; display_fb.pitch = DISPLAY_WIDTH; display_fb.pixelformat = SCE_DISPLAY_PIXELFORMAT_A8B8G8R8; display_fb.width = DISPLAY_WIDTH; display_fb.height = DISPLAY_HEIGHT; sceDisplaySetFrameBuf(&display_fb, SCE_DISPLAY_SETBUF_NEXTFRAME); sceDisplayWaitVblankStart(); }

Указатель выносится в отдельную структуру, а не передаётся в 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)

main.c
/* as before */ SceGxmInitializeParams gxm_init_params; /* as before */ gxm_init_params.flags = 0; // as before gxm_init_params.displayQueueMaxPendingCount = 1; gxm_init_params.displayQueueCallback = display_queue_callback; gxm_init_params.displayQueueCallbackDataSize = sizeof(struct DisplayCallbackData); /* as before */ gxm_init_params.parameterBufferSize = SCE_GXM_DEFAULT_PARAMETER_BUFFER_SIZE; /* as before */ sceGxmInitialize(&gxm_init_params);
Теперь gxm знает коллбек, который нужно вызывать в случае освобождения дисплейной очереди.

III.DB (Search03_db) Два буфера:
Чтобы поставить дисплейный буфер на ожидание в дисплейную очередь, нужны ещё и два примитива синхронизации типа SceGxmSyncObject. Они нужны gxm для того, чтобы синхронизировать операцию смены буфера с рендерингом. Примитивы синхронизации удобно создать одновременно с созданием дисплейного буфера. Так же теперь, для двойной буферизации, нужно два дисплейных буфера. Теперь шаг III выглядит так:
main.c
#define DISPLAY_BUFFER_COUNT 2 ... void *display_buffer[DISPLAY_BUFFER_COUNT]; SceGxmColorSurface display_surface[DISPLAY_BUFFER_COUNT]; SceGxmSyncObject *display_buffer_sync[DISPLAY_BUFFER_COUNT]; uint32_t buff_size = 4 * DISPLAY_WIDTH * DISPLAY_HEIGHT; for(uint32_t i = 0; i < DISPLAY_BUFFER_COUNT; i++) { display_buffer[i] = allocMapVideoMemory(buff_size); uint32_t *pixel = (uint32_t*)display_buffer[i]; for (uint32_t k = 0; k < buff_size / 4; k++) { pixel[k] = DEBUG_COLOR; } sceGxmColorSurfaceInit( &display_surface[i] , SCE_GXM_COLOR_FORMAT_A8B8G8R8 , SCE_GXM_COLOR_SURFACE_LINEAR , SCE_GXM_COLOR_SURFACE_SCALE_NONE , SCE_GXM_OUTPUT_REGISTER_SIZE_32BIT , DISPLAY_WIDTH , DISPLAY_HEIGHT , DISPLAY_WIDTH , display_buffer[i]); sceGxmSyncObjectCreate(&display_buffer_sync[i]); }
вместо создания одного экранного буфера (и одной поверхности), теперь создаются массивы их. Плюс здесь же для каждого экранного буфера создаются и примитивы синхронизации, т.к. они нужны для дисплейной очереди, и здесь самое подходящее место создать их.

IX.a_DB (Search9a_db)
Для исполнения двойной буферизации в главном цикле будут поочерёдно заполняться буферы, чередуясь кто из них передний, кто задний. Для этого сцена инициализируется указателем на задний буфер, а так же при инициализации сцены ей передаётся указатель на объект синхронизации заднего буфера:
main.c
static int back_index = 0; int front_index = back_index; back_index = !back_index; sceGxmBeginScene(context , 0 , render_target , NULL , NULL , display_buffer_sync[back_index] , &display_surface[back_index] , &surface_depth);

IX.б_DB (Search9б_db) Отрисовка - без изменений:
Здесь никаких изменений. Здесь то же самое заполнение очереди задач, от используемой буферизации не зависят команды для отрисовки примитивов.
main.c
/* as before */ sceGxmSetVertexProgram(context, vertex_program_patched); /* as before */ sceGxmSetFragmentProgram(context, fragment_program_patched); /* as before */ sceGxmSetVertexStream(context, 0, mesh_data); /* as before */ sceGxmDraw(context /* as before */ , SCE_GXM_PRIMITIVE_TRIANGLES /* as before */ , SCE_GXM_INDEX_FORMAT_U16 /* as before */ , indices_data /* as before */ , 3);

IX.в_DB (Search9в_db) окончание сцены - без изменений:
Точно так же никаких изменений. Просто указание библиотеке, что больше команд на отрисовку для этой сцены не будет.
main.c
/* as before */ sceGxmEndScene( /* as before */ context /* as before */ , NULL /* as before */ , NULL);

IX.г_DB (Search9г_db)
И вот теперь, вместо непосредственной отрисовки (после возврата управления из sceGxmFinish()), дисплейный буфер регистрируется в дисплейной очереди. В дисплейную очередь передаются также, вместе с адресом буфера, примитивы синхронизации текущего буфера и следующего.
main.c
struct DisplayCallbackData display_data; display_data.addr = display_buffer[back_index]; sceGxmDisplayQueueAddEntry( display_buffer_sync[front_index] , display_buffer_sync[back_index] , &display_data);
От главного цикла больше действий не требуется. Теперь работу по смене буфера осуществляет callback-функция, зарегестрированная в gxm для этой цели. Callback вызывается когда закончатся операции как старого, так и нового буфера, внутри текущего потока. В этот момент указатель на бекбуфер будет передан функции дисплейной библиотеки для отображения на экране, и она выведет его на экран так же, как и в главном цикле ранее.

X (SearX)
Последовательно освобождаются все использованные ресурсы
main.c
// Wait untill all rendering done so didn't delete data in use sceGxmFinish(context); // Deleting mesh deallocVideoMemory(mesh_data); deallocVideoMemory(indices_data); // Wait untill all queued jobs done so didn't delete resource in use sceGxmDisplayQueueFinish(); // Deleting depth buffer deallocVideoMemory(depth_buffer); for (uint32_t i = 0; i < DISPLAY_BUFFER_COUNT; ++i) { // Freeing videomemory deallocVideoMemory(display_buffer[i]); // Deleting sync object sceGxmSyncObjectDestroy(display_buffer_sync[i]); } // Deleting shaders sceGxmShaderPatcherReleaseVertexProgram(shader_patcher, vertex_program_patched); sceGxmShaderPatcherUnregisterProgram(shader_patcher, vertex_program_id); sceGxmShaderPatcherReleaseFragmentProgram(shader_patcher , fragment_program_patched); sceGxmShaderPatcherUnregisterProgram(shader_patcher, fragment_program_id); // Destroying shader patcher sceGxmShaderPatcherDestroy(shader_patcher); sceGxmUnmapVertexUsseMemory(shader_patcher_params.vertexUsseMem); deallocVideoMemory(shader_patcher_params.vertexUsseMem); sceGxmUnmapFragmentUsseMemory(shader_patcher_params.fragmentUsseMem); deallocVideoMemory(shader_patcher_params.fragmentUsseMem); deallocVideoMemory(shader_patcher_params.bufferMem); // Destroying RenderTarget sceGxmDestroyRenderTarget(render_target); // Destroying Context sceGxmDestroyContext(context); sceGxmUnmapFragmentUsseMemory(context_param.fragmentUsseRingBufferMem); deallocVideoMemory(context_param.fragmentUsseRingBufferMem); deallocVideoMemory(context_param.vdmRingBufferMem); deallocVideoMemory(context_param.vertexRingBufferMem); deallocVideoMemory(context_param.fragmentRingBufferMem); free(context_param.hostMem); // Uninitializing libgxm sceGxmTerminate();