Capas de GLES

En dispositivos con Android 10 (nivel de API 29) y versiones posteriores, están disponibles las capas de OpenGL ES (GLES). Una app depurable puede cargar capas de GLES desde su APK, desde su directorio base o desde un APK de capas seleccionado.

El uso de capas de GLES es similar al uso de la capa de validación de Vulkan.

Requisitos

Las capas de GLES son solo compatibles con la versión de GLES 2.0 y versiones posteriores.

Inicialización de la capa

Después de propagar los puntos de entrada estándar, EGL Loader instancia un objeto LayerLoader de GLES. Si las capas de depuración están habilitadas, el objeto LayerLoader analiza los directorios especificados en busca de capas, como lo hace el cargador de Vulkan.

Si las capas están habilitadas, LayerLoader busca las capas especificadas y las enumera en una lista con nombres de archivos separados por dos puntos.

LayerLoader desvía las capas en el orden que tú especifiques, por lo que la primera capa aparece directamente debajo de la aplicación. Por cada capa, LayerLoader hace un seguimiento de los puntos de entrada AndroidGLESLayer_Initialize y AndroidGLESLayer_GetProcAddress. Las capas deben proporcionar esas interfaces para que puedan cargarse.

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

AndroidGLESLayer_Initialize() proporciona un identificador para que lo utilice la capa (layer_id) y un punto de entrada que puede llamarse para buscar las funciones que se encuentran debajo de la capa. El punto de entrada se puede usar en la siguiente muestra de código:

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

AndroidGLESLayer_GetProcAddress toma la dirección de la próxima llamada en la cadena a la que la capa debería llamar al finalizar. Si existe solo una capa, next apunta directamente al controlador de la mayoría de las funciones.

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

Por cada capa que LayerLoader de GLES encuentra, llama a AndroidGLESLayer_Initialize, recorre la lista de funciones de libEGL y llama a AndroidGLESLayer_GetProcAddress para todas las funciones conocidas. La capa es la que debe determinar cómo hacer un seguimiento de la próxima dirección. Si la capa intercepta una función, hace un seguimiento de la dirección de la función. Si la capa no intercepta una función, AndroidGLESLayer_GetProcAddress muestra la misma dirección de función que le pasaron. Luego, LayerLoader actualiza la lista de hooks de funciones para que apunte al punto de entrada de la capa.

No es necesario que las capas realicen una acción con la información que AndroidGLESLayer_Initialize y get_next_layer_proc_address proporcionan, pero proporcionar los datos facilita que las capas existentes, como el Inspector de GPU de Android y RenderDoc, admitan Android. Con esos datos, una capa puede buscar funciones independientemente, en lugar de esperar llamadas a AndroidGLESLayer_GetProcAddress. Si las capas eligen inicializarse antes de que el cargador haya buscado todos los puntos de entrada, deben usar get_next_layer_proc_address. eglGetProcAddress debe pasar por la cadena hacia la plataforma.

Cómo colocar las capas

LayerLoader de GLES busca capas en las siguientes ubicaciones, en orden de prioridad:

1. Ubicación del sistema para la raíz

Esta acción requiere acceso de raíz:

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. Directorio base de la aplicación

La aplicación de destino debe poder depurarse, o bien debes tener acceso a la raíz:

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 externo

Determina la ABI de tu aplicación objetivo, luego instala un APK que contiene las capas que quieres cargar:

adb install --abi armeabi-v7a layers.apk

4. En el APK de la aplicación objetivo

En el siguiente ejemplo, se muestra cómo colocar capas en el APK de la aplicación:

$ 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

Cómo habilitar las capas

Puedes habilitar esas capas por app o de forma global. La configuración por app persiste después de los reinicios, mientras que las propiedades globales se borran.

El modelo de seguridad y las políticas de Android difieren significativamente de los de otras plataformas. Para cargar capas externas, una de las siguientes opciones debe ser verdadera:

  • El archivo de manifiesto de la app de destino incluye el siguiente elemento de metadatos (solo se aplica a apps que se orientan a Android 11 [nivel de API 30] o versiones posteriores):

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

    Debes usar esta opción para generar el perfil de tu aplicación.

  • La app de destino es depurable. Esta opción te brinda más información de depuración, pero puede afectar negativamente el rendimiento de la app.

  • La app de destino se ejecuta en una compilación userdebug del sistema operativo que otorga acceso de raíz.

Para habilitar las capas por app, haz lo siguiente:

# 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>

Para inhabilitar las capas por app, haz lo siguiente:

# 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

Para habilitar las capas globalmente, haz lo siguiente:

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

Cómo crear una capa

Las capas deben exponer las siguientes dos funciones descritas en Inicialización de EGL Loader:

AndroidGLESLayer_Initialize
AndroidGLESLayer_GetProcAddress

Capas pasivas

Para una capa que solo intercepta algunas funciones, una capa inicializada en forma pasiva resulta óptima. La capa inicializada en forma pasiva espera a que LayerLoader de GLES inicialice la función que necesita.

En el siguiente ejemplo de código, se muestra cómo crear una capa pasiva.

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);
  }
}

Capas activas

Es necesario utilizar la inicialización de capas activas si se requieren capas más formalizadas que tienen que inicializarse directamente o capas que tienen que buscar extensiones no conocidas para EGL Loader. La capa usa el objeto get_next_layer_proc_address que AndroidGLESLayer_Initialize proporciona para buscar una función. La capa debe responder a las solicitudes de AndroidGLESLayer_GetProcAddress desde el cargador, de manera que la plataforma sepa dónde se enrutarán las llamadas. En el siguiente ejemplo de código, se muestra cómo crear una capa activa.

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);
  }
}