Couches GLES

Sur les appareils équipés d'Android 10 (niveau d'API 29) ou version ultérieure, la superposition de couches OpenGL ES (GLES) est disponible. Une application débogable peut charger des couches GLES depuis son APK, son répertoire de base ou un APK de couches sélectionné.

Les couches GLES s'utilisent de la même manière que la couche de validation Vulkan.

Conditions requises

Les couches GLES ne sont compatibles qu'avec GLES 2.0 ou version ultérieure.

Initialisation des couches

Après avoir inséré des points d'entrée standards, le chargeur EGL instancie un LayerLoader GLES. Si les couches de débogage sont activées, le LayerLoader recherche des couches dans les répertoires spécifiés, comme le fait le chargeur Vulkan.

Si la superposition de couches est activée, le LayerLoader recherche et recense la liste de couches spécifiée. Celle-ci est définie par des noms de fichiers séparés par un signe deux-points.

Le LayerLoader traverse les couches dans l'ordre que vous spécifiez, de sorte que la première couche se trouve juste en dessous de l'application. Pour chaque couche, le LayerLoader recense les points d'entrée AndroidGLESLayer_Initialize et AndroidGLESLayer_GetProcAddress. Les couches doivent fournir ces interfaces pour pouvoir être chargées.

typedef void* (*PFNEGLGETNEXTLAYERPROCADDRESSPROC)(void*, const char*);
void* AndroidGLESLayer_Initialize(void* layer_id, PFNEGLGETNEXTLAYERPROCADDRESSPROC get_next_layer_proc_address))

AndroidGLESLayer_Initialize() fournit un identifiant à utiliser par la couche (layer_id) et un point d'entrée pouvant être appelé pour rechercher des fonctions sous celle-ci. Le point d'entrée peut être utilisé comme illustré dans l'exemple de code suivant :

const char* func = "eglFoo";
void* gpa = get_next_layer_proc_address(layer_id, func);

AndroidGLESLayer_GetProcAddress obtient l'adresse du prochain appel de la chaîne que la couche doit appeler lorsqu'elle a terminé. S'il n'y a qu'une seule couche, next pointe directement vers le pilote pour la plupart des fonctions.

typedef __eglMustCastToProperFunctionPointerType EGLFuncPointer;
void* AndroidGLESLayer_GetProcAddress(const char *funcName, EGLFuncPointer next)

Pour chaque couche qu'il trouve, le LayerLoader GLES appelle AndroidGLESLayer_Initialize, parcourt les listes de fonctions libEGL et appelle AndroidGLESLayer_GetProcAddress pour toutes les fonctions connues. C'est à la couche de déterminer comment recenser l'adresse suivante. Si la couche intercepte une fonction, elle recense l'adresse de la fonction. Si la couche n'intercepte pas de fonction, AndroidGLESLayer_GetProcAddress renvoie l'adresse de fonction qui lui a été transmise. Le LayerLoader met ensuite à jour la liste de hooks de fonction pour qu'elle pointe vers le point d'entrée de la couche.

Les couches ne sont pas tenues de traiter les informations fournies par AndroidGLESLayer_Initialize et get_next_layer_proc_address, mais la transmission de ces données facilite la compatibilité avec Android pour les couches existantes comme Android GPU Inspector et RenderDoc. Avec ces données, une couche peut rechercher des fonctions de manière indépendante au lieu d'attendre que AndroidGLESLayer_GetProcAddress soit appelé. Si les couches choisissent de s'initialiser avant que le chargeur n'ait interrogé tous les points d'entrée, elles doivent utiliser get_next_layer_proc_address. eglGetProcAddress doit être transmis tout au long de la chaîne jusqu'à la plate-forme.

Placer les couches

Le LayerLoader GLES recherche des couches aux emplacements suivants, par ordre de priorité :

1. Emplacement système de la racine

Un accès racine est nécessaire.

adb root
adb disable-verity
adb reboot
adb root
adb shell setenforce 0
adb shell mkdir -p /data/local/debug/gles
adb push <layer>.so /data/local/debug/gles/

2. Répertoire de base de l'application

L'application cible doit être débogable ou vous devez disposer d'un accès racine :

adb push libGLTrace.so /data/local/tmp
adb shell run-as com.android.gl2jni cp /data/local/tmp/libGLTrace.so .
adb shell run-as com.android.gl2jni ls | grep libGLTrace
libGLTrace.so

3. APK externe

Déterminez l'ABI de votre application cible, puis installez un APK contenant les couches que vous souhaitez charger :

adb install --abi armeabi-v7a layers.apk

4. Dans l'APK de l'application cible

L'exemple suivant montre comment placer des couches dans l'APK de l'application :

$ jar tf GLES_layers.apk
lib/arm64-v8a/libGLES_glesLayer1.so
lib/arm64-v8a/libGLES_glesLayer2.so
lib/arm64-v8a/libGLES_glesLayer3.so
lib/armeabi-v7a/libGLES_glesLayer1.so
lib/armeabi-v7a/libGLES_glesLayer2.so
lib/armeabi-v7a/libGLES_glesLayer3.so
resources.arsc
AndroidManifest.xml
META-INF/CERT.SF
META-INF/CERT.RSA
META-INF/MANIFEST.MF

Activer les couches

Vous pouvez activer les couches GLES pour une application ou de manière globale. Les paramètres par application sont conservés après le redémarrage, tandis que les propriétés globales sont effacées.

Le modèle de sécurité et les règles d'Android sont très différents de ceux des autres plates-formes. Le chargement de couches externes nécessite que l'une des conditions suivantes soit remplie :

  • Le fichier manifeste de l'application cible inclut l'élément meta-data suivant (concerne uniquement les applications qui ciblent Android 11 (niveau d'API 30) ou version ultérieure) :

    <meta-data android:name="com.android.graphics.injectLayers.enable" android:value="true" />

    Utilisez cette option pour effectuer le profilage de votre application.

  • L'application cible est débogable. Cette option vous fournit davantage d'informations de débogage, mais peut affecter les performances de votre application.

  • L'application cible est exécutée sur un build userdebug du système d'exploitation qui accorde un accès racine.

Pour activer les couches pour une application :

# Enable layers
adb shell settings put global enable_gpu_debug_layers 1

# Specify target application
adb shell settings put global gpu_debug_app <package_name>

# Specify layer list (from top to bottom)
# Layers are identified by their filenames, such as "libGLLayer.so"
adb shell settings put global gpu_debug_layers_gles <layer1:layer2:layerN>

# Specify packages to search for layers
adb shell settings put global gpu_debug_layer_app <package1:package2:packageN>

Pour désactiver les couches pour une application :

# Delete the global setting that enables layers
adb shell settings delete global enable_gpu_debug_layers

# Delete the global setting that selects target application
adb shell settings delete global gpu_debug_app

# Delete the global setting that specifies layer list
adb shell settings delete global gpu_debug_layers_gles

# Delete the global setting that specifies layer packages
adb shell settings delete global gpu_debug_layer_app

Pour activer les couches de manière globale :

# This attempts to load layers for all applications, including native
# executables
adb shell setprop debug.gles.layers <layer1:layer2:layerN>

Créer une couche

Les couches doivent exposer les deux fonctions suivantes décrites dans Initialisation du chargeur EGL :

AndroidGLESLayer_Initialize
AndroidGLESLayer_GetProcAddress

Couches passives

Si elle n'intercepte qu'une poignée de fonctions, une couche initialisée de manière passive est optimale. La couche initialisée de manière passive attend que le LayerLoader GLES initialise la fonction dont elle a besoin.

L'exemple de code suivant montre comment créer une couche passive.

namespace {

std::unordered_map<std::string, EGLFuncPointer> funcMap;

EGLAPI EGLBoolean EGLAPIENTRY glesLayer_eglChooseConfig (
  EGLDisplay dpy, const EGLint *attrib_list, EGLConfig *configs, EGLint config_size,
  EGLint *num_config) {

  EGLFuncPointer entry = funcMap["eglChooseConfig"];

  typedef EGLBoolean (*PFNEGLCHOOSECONFIGPROC)(
    EGLDisplay, const EGLint*, EGLConfig*, EGLint, EGLint*);

  PFNEGLCHOOSECONFIGPROC next = reinterpret_cast<PFNEGLCHOOSECONFIGPROC>(entry);

  return next(dpy, attrib_list, configs, config_size, num_config);
}

EGLAPI EGLFuncPointer EGLAPIENTRY eglGPA(const char* funcName) {

  #define GETPROCADDR(func) if(!strcmp(funcName, #func)) { \
    return (EGLFuncPointer)glesLayer_##func; }

  GETPROCADDR(eglChooseConfig);

  // Don't return anything for unrecognized functions
  return nullptr;
}

EGLAPI void EGLAPIENTRY glesLayer_InitializeLayer(
  void* layer_id, PFNEGLGETNEXTLAYERPROCADDRESSPROC get_next_layer_proc_address) {
     // This function is purposefully empty, since this layer does not proactively
     // look up any entrypoints
  }

EGLAPI EGLFuncPointer EGLAPIENTRY glesLayer_GetLayerProcAddress(
  const char* funcName, EGLFuncPointer next) {
  EGLFuncPointer entry = eglGPA(funcName);
  if (entry != nullptr) {
    funcMap[std::string(funcName)] = next;
    return entry;
  }
  return next;
}

}  // namespace

extern "C" {
  __attribute((visibility("default"))) EGLAPI void AndroidGLESLayer_Initialize(
    void* layer_id, PFNEGLGETNEXTLAYERPROCADDRESSPROC get_next_layer_proc_address) {
    return (void)glesLayer_InitializeLayer(layer_id, get_next_layer_proc_address);
  }
  __attribute((visibility("default"))) EGLAPI void* AndroidGLESLayer_GetProcAddress(
    const char *funcName, EGLFuncPointer next) {
    return (void*)glesLayer_GetLayerProcAddress(funcName, next);
  }
}

Couches actives

Pour les couches plus formelles qui doivent s'initialiser complètement à l'avance ou celles qui doivent rechercher des extensions que le chargeur EGL ne connaît pas, une initialisation active est requise. La couche utilise get_next_layer_proc_address fourni par AndroidGLESLayer_Initialize pour rechercher une fonction. Elle doit tout de même répondre aux requêtes AndroidGLESLayer_GetProcAddress du chargeur pour que la plate-forme sache où acheminer les appels. L'exemple de code suivant montre comment créer une couche active.

namespace {

std::unordered_map<std::string, EGLFuncPointer> funcMap;

EGLAPI EGLBoolean EGLAPIENTRY glesLayer_eglChooseConfig (
  EGLDisplay dpy, const EGLint *attrib_list, EGLConfig *configs, EGLint config_size,
  EGLint *num_config) {

  EGLFuncPointer entry = funcMap["eglChooseConfig"];

  typedef EGLBoolean (*PFNEGLCHOOSECONFIGPROC)(
    EGLDisplay, const EGLint*, EGLConfig*, EGLint, EGLint*);

  PFNEGLCHOOSECONFIGPROC next = reinterpret_cast<PFNEGLCHOOSECONFIGPROC>(entry);

  return next(dpy, attrib_list, configs, config_size, num_config);
}

EGLAPI EGLFuncPointer EGLAPIENTRY eglGPA(const char* funcName) {

  #define GETPROCADDR(func) if(!strcmp(funcName, #func)) { \
    return (EGLFuncPointer)glesLayer_##func; }

  GETPROCADDR(eglChooseConfig);

  // Don't return anything for unrecognized functions
  return nullptr;
}

EGLAPI void EGLAPIENTRY glesLayer_InitializeLayer(
  void* layer_id, PFNEGLGETNEXTLAYERPROCADDRESSPROC get_next_layer_proc_address) {

  // Note: This is where the layer would populate its function map with all the
  // functions it cares about
  const char* func = eglChooseConfig;
  funcMap[func] = get_next_layer_proc_address(layer_id, func);
}

EGLAPI EGLFuncPointer EGLAPIENTRY glesLayer_GetLayerProcAddress(
  const char* funcName, EGLFuncPointer next) {
  EGLFuncPointer entry = eglGPA(funcName);
  if (entry != nullptr) {
    return entry;
  }

  return next;
}

}  // namespace

extern "C" {
  __attribute((visibility("default"))) EGLAPI void AndroidGLESLayer_Initialize(
    void* layer_id, PFNEGLGETNEXTLAYERPROCADDRESSPROC get_next_layer_proc_address) {
    return (void)glesLayer_InitializeLayer(layer_id, get_next_layer_proc_address);
  }
  __attribute((visibility("default"))) EGLAPI void* AndroidGLESLayer_GetProcAddress(
    const char *funcName, EGLFuncPointer next) {
    return (void*)glesLayer_GetLayerProcAddress(funcName, next);
  }
}