GLES 레이어

Android 10(API 수준 29) 이상을 실행하는 기기에서 OpenGL ES(GLES) 레이어링을 사용할 수 있습니다. 디버그 가능한 앱은 APK, 기본 디렉터리, 선택한 레이어 APK 중 하나에서 GLES 레이어를 로드할 수 있습니다.

GLES 레이어 사용법은 Vulkan 유효성 검사 계층 사용법과 비슷합니다.

요구사항

GLES 레이어는 GLES 2.0 이상 버전에서만 지원됩니다.

레이어 초기화

표준 진입점을 채운 이후 EGL Loader는 GLES LayerLoader를 인스턴스화합니다. 디버그 레이어가 사용 설정되었다면 LayerLoaderVulkan 로더가 하는 것처럼 지정 디렉터리에서 레이어를 검색합니다.

레이어링이 사용 설정되었다면 LayerLoader는 지정된 레이어 목록을 검색하여 열거합니다. 레이어 목록은 콜론으로 구분된 파일 이름으로 지정됩니다.

LayerLoader는 지정된 순서대로 레이어를 탐색하므로, 첫 번째 레이어는 애플리케이션 바로 아래에 위치합니다. LayerLoader는 각 레이어에 대해 AndroidGLESLayer_InitializeAndroidGLESLayer_GetProcAddress 진입점을 추적합니다. 레이어는 이 인터페이스를 로드 가능하도록 제공해야 합니다.

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

AndroidGLESLayer_Initialize()는 레이어가 사용할 식별자(layer_id) 및 레이어 아래에서 함수를 조회하기 위해 호출할 수 있는 진입점을 제공합니다. 다음 코드 샘플에서와 같이 진입점을 사용할 수 있습니다.

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

AndroidGLESLayer_GetProcAddress는 완료 시 레이어가 호출해야 할 체인 내 다음 호출의 주소를 가져옵니다. 레이어가 하나만 있다면 next는 대부분의 함수에 대해 드라이버를 직접 가리킵니다.

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

GLES LayerLoader가 찾은 각 레이어에 대해 GLES LayerLoader는 AndroidGLESLayer_Initialize를 호출하고, libEGL의 함수 목록을 탐색하고, 알려진 모든 함수에 대해 AndroidGLESLayer_GetProcAddress를 호출합니다. 레이어에 따라 다음 주소를 추적하는 방법이 결정됩니다. 레이어가 함수를 가로채면 함수의 주소를 추적합니다. 레이어가 함수를 가로채지 않으면 AndroidGLESLayer_GetProcAddress는 전달된 것과 동일한 함수 주소를 반환합니다. 그런 다음, LayerLoader는 함수 후크 목록을 업데이트하여 레이어의 진입점을 가리킵니다.

레이어는 AndroidGLESLayer_Initializeget_next_layer_proc_address가 제공하는 정보로 아무것도 할 필요가 없습니다. 그러나 데이터를 제공하면 Android GPU 검사기RenderDoc과 같은 기존 레이어가 더 쉽게 Android를 지원할 수 있습니다. 이 데이터를 통해 레이어는 AndroidGLESLayer_GetProcAddress 호출을 대기하지 않고 독립적으로 함수를 조회할 수 있습니다. 로더가 모든 진입점을 쿼리하기 전에 레이어가 자체적으로 초기화하도록 선택한다면 레이어는 get_next_layer_proc_address를 사용해야 합니다. eglGetProcAddress는 체인을 따라 플랫폼에 전달되어야 합니다.

레이어 위치

GLES LayerLoader는 다음 위치에서 우선순위에 따라 레이어를 검색합니다.

1. 시스템의 루트 위치

루트 액세스 권한이 필요합니다.

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. 애플리케이션의 기본 디렉터리

타겟 애플리케이션이 디버그 가능하거나 개발자에게 루트 액세스 권한이 있어야 합니다.

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

타겟 애플리케이션의 ABI를 결정한 다음 로드할 레이어가 포함된 APK를 설치합니다.

adb install --abi armeabi-v7a layers.apk

4. 타겟 애플리케이션의 APK

다음 예에서는 애플리케이션 APK에서 레이어를 배치하는 방법을 보여줍니다.

$ 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

레이어 사용 설정

전역적으로 또는 앱별로 GLES 레이어를 사용 설정할 수 있습니다. 앱별 설정은 재부팅을 거쳐도 지속되는 반면, 전역 속성은 재부팅 시 지워집니다.

Android의 보안 모델 및 정책은 다른 플랫폼과 크게 다릅니다. 외부 레이어를 로드하려면 다음 중 하나가 true여야 합니다.

  • 타겟 앱의 매니페스트 파일에는 다음 meta-data 요소가 포함됩니다(Android 11(API 수준 30) 이상을 타겟팅하는 앱에만 적용됨).

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

    이 옵션을 사용하여 애플리케이션을 프로파일링해야 합니다.

  • 타겟 앱이 디버그 가능합니다. 이 옵션은 추가 디버그 정보를 제공하지만 앱 성능에 부정적인 영향을 줄 수 있습니다.

  • 타겟 앱이 루트 액세스 권한을 부여하는 운영체제의 userdebug 빌드에서 실행됩니다.

앱별로 레이어 사용 설정하기:

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

앱별로 레이어 사용 중지하기:

# 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

전역적으로 레이어 사용 설정하기:

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

레이어 만들기

레이어는 EGL Loader 초기화의 설명에 따라 다음 두 함수를 노출해야 합니다.

AndroidGLESLayer_Initialize
AndroidGLESLayer_GetProcAddress

수동적 레이어

함수 몇 개만 가로채는 레이어의 경우 수동적 초기화 레이어가 최적입니다. 수동적 초기화 레이어는 GLES LayerLoader가 필요한 함수를 초기화할 때까지 기다립니다.

다음 코드 샘플은 수동적 레이어를 만드는 방법을 보여줍니다.

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

능동적 레이어

완전히 초기화해야 하는 더 형식화된 레이어나 EGL Loader에게 알려지지 않은 확장자를 찾아야 하는 레이어의 경우 능동적 레이어 초기화가 필요합니다. 이러한 레이어는 AndroidGLESLayer_Initialize가 제공하는 get_next_layer_proc_address를 사용하여 함수를 찾습니다. 플랫폼이 호출을 라우팅할 위치를 알 수 있도록 레이어는 여전히 로더의 AndroidGLESLayer_GetProcAddress 요청에 응답해야 합니다. 다음 코드 샘플은 능동적 레이어를 만드는 방법을 보여줍니다.

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