Для рабочих нагрузок, где идеально подходят вычисления на графическом процессоре, миграция сценариев RenderScript на OpenGL ES (GLES) позволяет приложениям, написанным на Kotlin, Java или с использованием NDK, использовать преимущества оборудования графического процессора. Ниже приводится общий обзор, который поможет вам использовать вычислительные шейдеры OpenGL ES 3.1 для замены сценариев RenderScript.
Инициализация GLES
Вместо создания объекта контекста RenderScript выполните следующие шаги, чтобы создать закадровый контекст GLES с помощью EGL:
Получить отображение по умолчанию
Инициализируйте EGL, используя дисплей по умолчанию, указав версию GLES.
Выберите конфигурацию EGL с типом поверхности
EGL_PBUFFER_BIT
.Используйте дисплей и конфигурацию для создания контекста EGL.
Создайте закадровую поверхность с помощью
eglCreatePBufferSurface
. Если контекст будет использоваться только для вычислений, это может быть тривиально маленькая (1x1) поверхность.Создайте поток рендеринга и вызовите
eglMakeCurrent
в потоке рендеринга с контекстом отображения, поверхности и EGL, чтобы привязать контекст GL к потоку.
Пример приложения демонстрирует, как инициализировать контекст GLES в GLSLImageProcessor.kt
. Дополнительные сведения см. в разделах EGLSurfaces и OpenGL ES .
Отладочный вывод GLES
Получение полезных ошибок из OpenGL использует расширение для включения ведения журнала отладки, которое устанавливает обратный вызов вывода отладки. Метод glDebugMessageCallbackKHR
для этого из SDK никогда не был реализован и выдает исключение . Пример приложения включает оболочку для обратного вызова из кода NDK.
Распределения GLES
Выделение RenderScript можно перенести в неизменяемую текстуру хранилища или в объект буфера хранения шейдеров . Для изображений, доступных только для чтения, вы можете использовать объект Sampler , который позволяет фильтровать.
Ресурсы GLES распределяются внутри GLES. Чтобы избежать накладных расходов на копирование памяти при взаимодействии с другими компонентами Android, существует расширение для KHR Images , которое позволяет совместно использовать двумерные массивы данных изображений. Это расширение требуется для устройств Android, начиная с Android 8.0. Библиотека Android Jetpack с графическим ядром включает поддержку создания этих изображений в управляемом коде и сопоставления их с выделенным HardwareBuffer
:
val outputBuffers = Array(numberOfOutputImages) {
HardwareBuffer.create(
width, height, HardwareBuffer.RGBA_8888, 1,
HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE
)
}
val outputEGLImages = Array(numberOfOutputImages) { i ->
androidx.opengl.EGLExt.eglCreateImageFromHardwareBuffer(
display,
outputBuffers[i]
)!!
}
К сожалению, это не создает неизменяемую текстуру хранилища, необходимую вычислительному шейдеру для записи непосредственно в буфер. В примере используется glCopyTexSubImage2D
для копирования текстуры хранилища, используемой вычислительным шейдером, в KHR Image
. Если драйвер OpenGL поддерживает расширение EGL Image Storage , то это расширение можно использовать для создания общей неизменяемой текстуры хранилища, чтобы избежать копирования.
Преобразование в вычислительные шейдеры GLSL
Ваши сценарии RenderScript преобразуются в вычислительные шейдеры GLSL.
Напишите вычислительный шейдер GLSL.
В OpenGL ES вычислительные шейдеры написаны на языке шейдеров OpenGL (GLSL).
Адаптация глобальных переменных скрипта
В зависимости от характеристик глобальных переменных скрипта вы можете использовать юниформы или универсальные буферные объекты для глобальных переменных, которые не изменяются в шейдере:
- Единый буфер : рекомендуется для часто изменяемых глобальных переменных скрипта, размеры которых превышают предел push-константы.
Для глобальных переменных, которые изменяются внутри шейдера, вы можете использовать неизменяемую текстуру хранилища или объект буфера хранения шейдера .
Выполнение вычислений
Вычислительные шейдеры не являются частью графического конвейера; они универсальны и предназначены для вычисления задач с высокой степенью параллелизации. Это дает вам больше контроля над тем, как они выполняются, но это также означает, что вам нужно немного больше понимать, как распараллеливается ваша работа.
Создайте и инициализируйте вычислительную программу
Создание и инициализация вычислительной программы имеет много общего с работой с любым другим шейдером GLES.
Создайте программу и связанный с ней вычислительный шейдер.
Прикрепите исходный код шейдера, скомпилируйте шейдер (и проверьте результаты компиляции).
Прикрепите шейдер, свяжите программу и пользуйтесь программой.
Создайте, инициализируйте и привяжите любые униформы.
Начать расчет
Вычислительные шейдеры работают в абстрактном 1D, 2D или 3D-пространстве в ряде рабочих групп, которые определены в исходном коде шейдера и представляют минимальный размер вызова, а также геометрию шейдера. Следующий шейдер работает с 2D-изображением и определяет рабочие группы в двух измерениях:
private const val WORKGROUP_SIZE_X = 8
private const val WORKGROUP_SIZE_Y = 8
private const val ROTATION_MATRIX_SHADER =
"""#version 310 es
layout (local_size_x = $WORKGROUP_SIZE_X, local_size_y = $WORKGROUP_SIZE_Y, local_size_z = 1) in;
Рабочие группы могут совместно использовать память, определенную GL_MAX_COMPUTE_SHARED_MEMORY_SIZE
, размер которой составляет не менее 32 КБ, и могут использовать memoryBarrierShared()
для обеспечения согласованного доступа к памяти.
Определить размер рабочей группы
Даже если ваше проблемное пространство хорошо работает с размером рабочей группы, равным 1, установка соответствующего размера рабочей группы важна для распараллеливания вычислительного шейдера. Если размер слишком мал, драйвер графического процессора может, например, недостаточно распараллелить ваши вычисления. В идеале эти размеры должны быть настроены для каждого графического процессора, хотя разумные значения по умолчанию достаточно хорошо работают на современных устройствах, например размер рабочей группы 8x8 во фрагменте шейдера.
Существует GL_MAX_COMPUTE_WORK_GROUP_COUNT
, но он существенный; по спецификации оно должно быть не менее 65535 по всем трем осям.
Отправка шейдера
Последним шагом в выполнении вычислений является отправка шейдера с помощью одной из функций диспетчеризации, например glDispatchCompute
. Функция диспетчеризации отвечает за настройку количества рабочих групп для каждой оси:
GLES31.glDispatchCompute(
roundUp(inputImage.width, WORKGROUP_SIZE_X),
roundUp(inputImage.height, WORKGROUP_SIZE_Y),
1 // Z workgroup size. 1 == only one z level, which indicates a 2D kernel
)
Чтобы вернуть значение, сначала дождитесь завершения операции вычисления с использованием барьера памяти:
GLES31.glMemoryBarrier(GLES31.GL_SHADER_IMAGE_ACCESS_BARRIER_BIT)
Чтобы объединить несколько ядер (например, для миграции кода с помощью ScriptGroup
), создайте и отправьте несколько программ и синхронизируйте их доступ к выходным данным с помощью барьеров памяти.
Пример приложения демонстрирует две вычислительные задачи:
- Вращение HUE: вычислительная задача с одним вычислительным шейдером. Пример кода см. в
GLSLImageProcessor::rotateHue
. - Размытие: более сложная вычислительная задача, в которой последовательно выполняются два вычислительных шейдера. См. пример кода
GLSLImageProcessor::blur
.
Дополнительные сведения о барьерах памяти см. в разделах Обеспечение видимости и Общие переменные .