Migrer des scripts vers OpenGL ES 3.1

Pour les charges de travail où le calcul GPU est idéal, la migration des scripts RenderScript vers OpenGL ES (GLES) permet aux applications écrites en Kotlin, en Java ou à l'aide du NDK d'exploiter le matériel GPU. Vous trouverez ci-dessous une présentation générale qui vous aidera à utiliser les nuanceurs de calcul OpenGL ES 3.1 pour remplacer les scripts RenderScript.

Initialisation GLES

Au lieu de créer un objet de contexte RenderScript, procédez comme suit pour créer un contexte GLES hors écran avec EGL :

  1. Obtenir l'affichage par défaut

  2. Initialisez EGL à l'aide de l'affichage par défaut, en spécifiant la version GLES.

  3. Choisissez une configuration EGL avec un type de surface EGL_PBUFFER_BIT.

  4. Utilisez l'affichage et la configuration pour créer un contexte EGL.

  5. Créez la surface hors écran avec eglCreatePBufferSurface. Si le contexte est destiné à n'être utilisé que pour le calcul, il peut s'agir d'une surface particulièrement petite (1 x 1).

  6. Créez le thread de rendu et appelez eglMakeCurrent dans le thread de rendu avec le contexte d'affichage, de surface et EGL pour lier le contexte GL au thread.

L'application exemple montre comment initialiser le contexte GLES dans GLSLImageProcessor.kt. Pour en savoir plus, consultez EGLSurfaces et OpenGL ES.

Résultats du débogage GLES

Les erreurs utiles d'OpenGL utilisent une extension pour activer la journalisation de débogage qui définit un rappel de sortie de débogage. La méthode permettant d'effectuer cette opération à partir du SDK glDebugMessageCallbackKHR n'a jamais été mise en œuvre et génère une exception. L'application exemple inclut un wrapper pour le rappel à partir du code NDK.

Allocations GLES

Une allocation RenderScript peut être migrée vers une texture de stockage immuable ou un objet de tampon de stockage du nuanceur. Pour les images en lecture seule, vous pouvez utiliser un objet d'échantillonneur, qui permet le filtrage.

Les ressources GLES sont allouées dans GLES. Pour éviter les frais de copie de mémoire lors de l'interaction avec d'autres composants Android, il existe une extension pour les images KHR qui permet le partage de tableaux 2D de données d'image. Cette extension est requise pour les appareils Android à partir d'Android 8.0. La bibliothèque Android Jetpack graphics-core permet de créer ces images dans du code géré et de les associer à un HardwareBuffer alloué :

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]
    )!!
}

Malheureusement, cela ne crée pas la texture de stockage immuable requise pour qu'un nuanceur de calcul écrive directement dans le tampon. L'exemple utilise glCopyTexSubImage2D pour copier la texture de stockage utilisée par le nuanceur de calcul dans la KHR Image. Si le pilote OpenGL est compatible avec l'extension de stockage d'images EGL, celle-ci peut être utilisée pour créer une texture de stockage immuable partagée afin d'éviter la copie.

Conversion vers les nuanceurs de calcul GLSL

Vos scripts RenderScript sont convertis en nuanceurs de calcul GLSL.

Écrire un nuanceur de calcul GLSL

Dans OpenGL ES,les nuanceurs de calcul sont écrits dans le langage GLSL (OpenGL Shading Language).

Adaptation d'éléments généraux de script

En fonction des caractéristiques des éléments généraux de script, vous pouvez utiliser des objets uniformes ou des objets de tampon uniformes pour les éléments généraux qui ne sont pas modifiés dans le nuanceur :

  • Tampon uniforme : recommandé pour les éléments généraux de script fréquemment modifiés, de taille supérieure à la limite des constantes Push.

Pour les éléments généraux modifiés dans le nuanceur, vous pouvez utiliser une texture de stockage immuable ou un objet de tampon de stockage du nuanceur.

Exécuter des calculs

Les nuanceurs de calcul ne font pas partie du pipeline graphique. Ils sont destinés à un usage général et conçus pour calculer des tâches hautement parallélisables. Cela vous permet de mieux contrôler leur exécution, mais cela signifie également que vous devez en savoir un peu plus sur la façon dont votre tâche est parallélisée.

Créer et initialiser le programme de calcul

La création et l'initialisation du programme de calcul présentent de nombreuses similitudes avec l'utilisation de tout autre nuanceur GLES.

  1. Créez le programme et le nuanceur de calcul qui lui est associé.

  2. Associez la source du nuanceur, compilez-le et vérifiez les résultats de la compilation.

  3. Associez le nuanceur, liez le programme et utilisez ce dernier.

  4. Créez, initialisez et liez les éventuels objets uniformes.

Démarrer un calcul

Les nuanceurs de calcul s'exécutent dans un espace 1D, 2D ou 3D abstrait sur une série de groupes de travail, qui sont définis dans le code source du nuanceur et représentent la taille minimale d'appel ainsi que la géométrie du nuanceur. Le nuanceur suivant fonctionne sur une image 2D et définit les groupes de travail en deux dimensions :

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;

Les groupes de travail peuvent partager la mémoire, définie par GL_MAX_COMPUTE_SHARED_MEMORY_SIZE, d'au moins 32 Ko et peuvent utiliser memoryBarrierShared() pour fournir un accès cohérent à la mémoire.

Définir la taille du groupe de travail

Même si votre espace de problème fonctionne bien avec des groupes de travail de 1, il est important de définir une taille de groupe de travail appropriée pour paralléliser le nuanceur de calcul. Si la taille est trop faible, le pilote GPU risque de ne pas paralléliser suffisamment votre calcul, par exemple. Idéalement, ces tailles doivent être ajustées par GPU, bien que des valeurs par défaut raisonnables fonctionnent suffisamment bien sur les appareils actuels, tels que la taille de groupe de travail de 8 x 8 dans l'extrait de nuanceur.

Il existe un GL_MAX_COMPUTE_WORK_GROUP_COUNT, mais il est conséquent. Il doit être d'au moins 65 535 sur les trois axes, conformément à la spécification.

Distribuer le nuanceur

La dernière étape de l'exécution des calculs consiste à distribuer le nuanceur à l'aide de l'une des fonctions de distribution telles que glDispatchCompute. La fonction de distribution est chargée de définir le nombre de groupes de travail pour chaque axe :

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
)

Pour renvoyer la valeur, attendez d'abord la fin de l'opération de calcul à l'aide d'une barrière de mémoire :

GLES31.glMemoryBarrier(GLES31.GL_SHADER_IMAGE_ACCESS_BARRIER_BIT)

Pour enchaîner plusieurs noyaux (par exemple, pour migrer du code à l'aide de ScriptGroup), créez et distribuez plusieurs programmes et synchronisez leur accès à la sortie avec des barrières de mémoire.

L'application exemple présente deux tâches de calcul:

  • Rotation HUE : tâche de calcul avec un seul nuanceur de calcul. Consultez GLSLImageProcessor::rotateHue pour obtenir l'exemple de code.
  • Floutage : tâche de calcul plus complexe qui exécute de manière séquentielle deux nuanceurs de calcul. Consultez GLSLImageProcessor::blur pour obtenir l'exemple de code.

Pour en savoir plus sur les barrières de mémoire, consultez les sections Assurer la visibilité et Variables partagées.