Cómo migrar secuencias de comandos a OpenGL ES 3.1

Para las cargas de trabajo en las que el procesamiento con GPU es ideal, migrar secuencias de comandos de RenderScript a OpenGL ES (GLES) permite que las aplicaciones escritas en Kotlin, Java o que usan el NDK aprovechen el hardware de la GPU. A continuación, se incluye una descripción general de alto nivel que te ayudará a usar sombreadores de cómputos de OpenGL ES 3.1 para reemplazar las secuencias de comandos de RenderScript.

.

Inicialización de GLES

En lugar de crear un objeto de contexto de RenderScript, realiza los siguientes pasos para crear un contexto fuera de pantalla de GLES con EGL:

  1. Obtén la pantalla predeterminada.

  2. Inicializa EGL con la pantalla predeterminada y especifica la versión de GLES.

  3. Elige una configuración de EGL con un tipo de plataforma de EGL_PBUFFER_BIT.

  4. Usa la pantalla y la configuración para crear un contexto de EGL.

  5. Crea la superficie fuera de pantalla con eglCreatePBufferSurface. Si el contexto solo se usará para el procesamiento, esta puede ser una superficie trivialmente pequeña (1 x 1).

  6. Crea el subproceso de renderización y llama a eglMakeCurrent en él con la pantalla, la superficie y el contexto de EGL para vincular el contexto de GL al subproceso.

En la app de ejemplo, se muestra cómo inicializar el contexto de GLES en GLSLImageProcessor.kt. Para obtener más información, consulta EGLSurfaces y OpenGL ES.

Resultado de depuración de GLES

Para obtener errores útiles de OpenGL, se usa una extensión para habilitar el registro de depuración que establece una devolución de llamada de salida de depuración. El método para hacerlo desde el SDK, glDebugMessageCallbackKHR, nunca se implementó y arroja una excepción. La app de ejemplo incluye un wrapper para la devolución de llamada desde el código del NDK.

Asignaciones de GLES

Se puede migrar una asignación de RenderScript a una textura de almacenamiento inmutable o a un objeto Shader Storage Buffer. Para imágenes de solo lectura, puedes usar un objeto Sampler, que permite filtrar.

Los recursos de GLES se asignan en GLES. Para evitar la sobrecarga de copia de memoria cuando interactúas con otros componentes de Android, hay una extensión para imágenes KHR que permite compartir arreglos 2D de datos de imagen. Esta extensión es necesaria para los dispositivos Android a partir de Android 8.0. La biblioteca de Android Jetpack graphics-core incluye compatibilidad con la creación de esas imágenes dentro del código administrado y su asignación a una etiqueta 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]
    )!!
}

Lamentablemente, esto no crea la textura de almacenamiento inmutable necesaria para que un sombreador de cómputos escriba directamente en el búfer. En el ejemplo, se usa glCopyTexSubImage2D para copiar la textura de almacenamiento que usa el sombreador de cómputos en KHR Image. Si el controlador de OpenGL admite la extensión EGL Image Storage, se puede usar esa extensión para crear una textura de almacenamiento inmutable compartida y evitar la copia.

Conversión a ComputeShader GLSL

Tus secuencias de comandos de RenderScript se convierten en ComputeShader GLSL.

Escribe un ComputeShader GLSL

En OpenGL ES,los sombreadores de cómputos se escriben en OpenGL Shading Language (GLSL).

Adaptación de globales de secuencias de comandos

Según las características de los globales de secuencias de comandos, puedes usar objetos de búfer uniformes para los globales que no se modifiquen dentro del sombreador:

  • Búfer uniforme: Se recomienda para globales de secuencias de comandos que cambian con frecuencia y cuyo tamaño es mayor al límite de constante push.

Para los globales que se modifican dentro del sombreador, puedes usar una textura de almacenamiento inmutable o un objeto de búfer de almacenamiento de sombreador.

Ejecutar cálculos

Los sombreadores de cómputos no forman parte de la canalización de gráficos. son de uso general y están diseñados para procesar trabajos altamente paralelizables. Esto te permite tener más control sobre cómo se ejecutan, pero también significa que debes comprender un poco más cómo se paraleliza tu trabajo.

Crea e inicializa el programa de cómputos

La creación y la inicialización del programa de cómputos tienen mucho en común con el trabajo con cualquier otro sombreador de GLES.

  1. Crea el programa y el sombreador de cómputos asociado a él.

  2. Adjunta la fuente del sombreador, compila el sombreador (y verifica los resultados de la compilación).

  3. Adjunta el sombreador, vincula el programa y úsalo.

  4. Crea, inicializa y vincula cualquier uniforme.

Inicia un cómputo

Los sombreadores de cómputos funcionan dentro de un espacio abstracto 1D, 2D o 3D en una serie de grupos de trabajo, que se definen dentro del código fuente del sombreador y representan el tamaño mínimo de invocación, así como la geometría del sombreador. El siguiente sombreador funciona en una imagen 2D y define los grupos de trabajo en dos dimensiones:

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;

Los grupos de trabajo pueden compartir memoria, definida por GL_MAX_COMPUTE_SHARED_MEMORY_SIZE, que es de al menos 32 KB, y puede usar memoryBarrierShared() para proporcionar acceso coherente a la memoria.

Define el tamaño del grupo de trabajo

Incluso si el espacio del problema funciona bien con tamaños de grupo de trabajo de 1, es importante configurar un tamaño de grupo de trabajo adecuado para paralelizar el sombreador de cómputos. Si el tamaño es demasiado pequeño, es posible que el controlador de GPU no realice el procesamiento lo suficientemente pequeño, por ejemplo. Lo ideal sería que estos tamaños se ajustaran por GPU, aunque los valores predeterminados razonables funcionan lo suficientemente bien en dispositivos actuales, como el tamaño del grupo de trabajo de 8 × 8 en el fragmento del sombreador.

Hay un GL_MAX_COMPUTE_WORK_GROUP_COUNT, pero es sustancial; debe ser de al menos 65535 en los tres ejes según la especificación.

Cómo despachar el sombreador

El último paso para ejecutar cálculos es despachar el sombreador con una de las funciones de envío, como glDispatchCompute. La función de envío se encarga de configurar la cantidad de grupos de trabajo para cada eje:

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
)

Para mostrar el valor, primero espera a que termine la operación de cómputo con una barrera de memoria:

GLES31.glMemoryBarrier(GLES31.GL_SHADER_IMAGE_ACCESS_BARRIER_BIT)

Para encadenar varios kernels (por ejemplo, para migrar código con ScriptGroup), crea y envía varios programas y sincroniza su acceso al resultado con barreras de memoria.

En la app de ejemplo, se muestran dos tareas de cómputos:

  • Rotación de tonalidad: Es una tarea de cómputos con un solo sombreador de cómputos. Consulta GLSLImageProcessor::rotateHue para ver la muestra de código.
  • Desenfoque: Es una tarea de cómputos más compleja que ejecuta dos sombreadores de cómputos en forma secuencial. Consulta GLSLImageProcessor::blur para ver la muestra de código.

Para obtener más información sobre las barreras de memoria, consulta Cómo garantizar la visibilidad y Variables compartidas.