Camadas GLES

A criação de camadas com o OpenGL ES (GLES) está disponível em dispositivos com o Android 10 (API de nível 29) e versões mais recentes. Um app depurável pode carregar camadas GLES pelo APK dele, do diretório básico ou do APK de uma camada selecionada.

O uso de camadas GLES é semelhante ao uso de Camadas de validação da Vulkan.

Requisitos

Só há compatibilidade com camadas GLES a partir da versão 2.0.

Inicialização de camadas

Depois de preencher os pontos de entrada padrão, o EGL Loader instancia um LayerLoader do GLES. Assim como no carregador da Vulkan, se camadas de depuração forem ativadas, o LayerLoader verificará a existência de camadas nos diretórios especificados.

Se a criação de camadas estiver ativada, o LayerLoader pesquisará e enumerará uma lista especificada de camadas. A lista de camadas é especificada por nomes de arquivos separados por dois-pontos (:).

O LayerLoader transfere as camadas na ordem que você especifica. Assim, a primeira camada fica logo abaixo do aplicativo. Para cada camada, o LayerLoader rastreia os pontos de entrada AndroidGLESLayer_Initialize e AndroidGLESLayer_GetProcAddress. As camadas precisam disponibilizar essas interfaces para serem carregadas.

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

AndroidGLESLayer_Initialize() oferece um identificador a ser usado pela camada (layer_id) e um ponto de entrada que pode ser chamado para procurar funções abaixo da camada. O ponto de entrada pode ser usado na forma apresentada na amostra de código a seguir:

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

AndroidGLESLayer_GetProcAddress usa o endereço da chamada seguinte na cadeia que a camada precisa chamar ao concluir esse processo. Para a maior parte das funções, se houver somente uma camada, next apontará diretamente para o driver.

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

Para cada camada encontrada pelo LayerLoader do GLES, ele chama AndroidGLESLayer_Initialize, movimenta listas de funções de libEGL e chama AndroidGLESLayer_GetProcAddress para todas as funções conhecidas. Cabe à camada determinar como o próximo endereço será rastreado. Se a camada interceptar uma função, ela rastreará o endereço da função. Se a camada não interceptar uma função, AndroidGLESLayer_GetProcAddress retornará o mesmo endereço de função que foi transmitido. Então, o LayerLoader atualizará a lista de hooks de função para apontar para o ponto de entrada da camada.

As camadas não precisam fazer nada com as informações fornecidas por AndroidGLESLayer_Initialize e get_next_layer_proc_address, mas a disponibilidade dos dados facilita a compatibilidade das camadas existentes, como Android GPU Inspector e RenderDoc (links em inglês), com o Android. Com esses dados, a camada pode pesquisar funções de maneira independente, em vez de esperar chamadas para AndroidGLESLayer_GetProcAddress. Se as camadas forem inicializadas antes de o carregador ter consultado todos os pontos de entrada, elas precisarão usar get_next_layer_proc_address. O eglGetProcAddress precisa ser transmitido da cadeia para a plataforma.

Posicionar camadas

O LayerLoader do GLES procura camadas nos seguintes locais, em ordem de prioridade:

1. Local do sistema correspondente à raiz

Isso requer acesso à raiz.

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. Diretório básico do aplicativo

O aplicativo de destino precisa ser depurável. Caso contrário, você precisa ter acesso à raiz:

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

Defina a ABI do seu aplicativo de destino e instale um APK que contenha as camadas que você quer carregar:

adb install --abi armeabi-v7a layers.apk

4. No APK do aplicativo de destino

O exemplo a seguir mostra como posicionar camadas no APK do aplicativo:

$ 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

Ativar camadas

Você pode ativar as camadas GLES por app ou de forma global. As configurações por app persistem em reinicializações, enquanto as propriedades globais são apagadas na reinicialização.

As políticas e o modelo de segurança do Android são muito diferentes dos que são usados por outras plataformas. Para carregar camadas externas, uma das seguintes condições precisa ser verdadeira:

  • O arquivo de manifesto do app de destino inclui o seguinte elemento de metadados (aplica-se apenas a apps direcionados ao Android 11 (API de nível 30) ou versões mais recentes):

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

    Use essa opção para caracterizar o perfil do aplicativo.

  • O app de destino é depurável. Essa opção fornece mais informações de depuração, mas pode afetar negativamente o desempenho do app.

  • O app de destino é executado em um build userbug do sistema operacional que concede acesso raiz.

Para ativar camadas por app:

# 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 desativar camadas por app:

# 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 ativar camadas de forma global:

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

Criar uma camada

As camadas precisam expor as duas funções a seguir, descritas em Inicialização do EGL Loader:

AndroidGLESLayer_Initialize
AndroidGLESLayer_GetProcAddress

Camadas passivas

Uma camada inicializada de forma passiva é ideal para uma camada que intercepta apenas algumas funções. A camada inicializada de forma passiva aguarda até que o LayerLoader do GLES inicialize a função necessária.

A amostra de código a seguir demonstra como criar uma camada passiva.

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

Camadas ativas

Para camadas mais formalizadas que precisam ser totalmente inicializadas de imediato ou para camadas que precisam pesquisar extensões não conhecidas pelo EGL Loader, a inicialização ativa da camada é necessária. A camada usa o get_next_layer_proc_address que AndroidGLESLayer_Initialize fornece para procurar uma função. Ela ainda precisará responder às solicitações AndroidGLESLayer_GetProcAddress do carregador para que a plataforma saiba para onde encaminhar chamadas. A amostra de código a seguir demonstra como criar uma camada ativa.

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