Android의 Vulkan 유효성 검사 레이어

대부분의 명시적 그래픽 API는 오류 검사를 하면 성능이 저하될 수 있기 때문에 오류 검사를 하지 않습니다. Vulkan의 경우 개발 단계에서 오류 검사 기능을 사용한 후 앱의 출시 빌드에서는 이 기능을 제외하여 가장 중요한 시기에 성능이 저하되는 것을 방지하는 오류 검사 기능을 제공합니다. 이러한 방식으로 오류 검사 기능을 활용하려면 Vulkan 유효성 검사 레이어를 사용합니다. 유효성 검사 계층은 다양한 디버그 및 유효성 검사를 위해 Vulkan 진입점을 가로채거나 후크합니다.

유효성 검사 계층은 정의를 포함하고 있는 대상 진입점을 가로챕니다. 계층에 정의되지 않은 진입점은 유효하지 않은 기본 수준 드라이버에 도달합니다.

Android NDK 및 Vulkan 샘플에는 개발 중 사용할 수 있는 Vulkan 유효성 검사 계층이 포함되어 있습니다. 유효성 검사 계층을 그래픽 스택에 후크하여 유효성 검사 문제를 보고하도록 할 수 있습니다. 이러한 계측을 통해 개발 중 오용 문제를 파악하고 해결할 수 있습니다.

단일 Khronos 유효성 검사 레이어

Vulkan 계층은 로더에 의해 스택에 삽입되어 더 높은 수준의 계층이 아래의 계층을 호출하고 계층 스택이 결국 기기 드라이버에서 종료됩니다. 이전에는 여러 유효성 검사 계층이 Android에서 특정 순서로 사용 설정되었습니다. 그러나 지금은 이전의 모든 유효성 검사 계층 동작을 포괄하는 단일 계층 VK_LAYER_KHRONOS_validation이 있습니다. Vulkan 유효성 검사의 경우 모든 애플리케이션에서 단일 유효성 검사 계층 VK_LAYER_KHRONOS_validation을 사용 설정해야 합니다.

유효성 검사 레이어 패키징

NDK에는 미리 빌드된 유효성 검사 계층 바이너리가 포함되어 있어 이 바이너리를 APK에 패키징하여 테스트 기기로 푸시할 수 있습니다. 이 바이너리는 ndk-dir/sources/third_party/vulkan/src/build-android/jniLibs/abi/ 디렉터리에서 찾을 수 있습니다. 앱에서 요청하면 Vulkan 로더는 앱의 APK에서 계층을 찾아 로드합니다.

Gradle을 사용하여 APK에 유효성 검사 레이어 패키징

Android Gradle 플러그인 및 Android 스튜디오의 CMake 및 ndk-build 지원을 사용하여 프로젝트에 유효성 검사 계층을 추가할 수 있습니다. Android 스튜디오의 CMake 및 ndk-build 지원을 사용하여 라이브러리를 추가하려면 앱 모듈의 build.gradle 파일에 다음을 추가합니다.
sourceSets {
  main {
    jniLibs {
      // Gradle includes libraries in the following path as dependencies
      // of your CMake or ndk-build project so that they are packaged in
      // your app’s APK.
      srcDir "ndk-path/sources/third_party/vulkan/src/build-android/jniLibs"
    }
  }
}
Android 스튜디오의 CMake 및 ndk-build 지원에 관해 자세히 알아보려면 C 및 C++ 코드를 프로젝트에 추가를 참조하세요.

JNI 라이브러리에 유효성 검사 레이어 패키징

다음 명령줄 옵션을 사용하여 수동으로 유효성 검사 계층 바이너리를 프로젝트의 JNI 라이브러리 디렉터리에 추가할 수 있습니다.
$ cd project-root
$ mkdir -p app/src/main
$ cp -fr ndk-path/sources/third_party/vulkan/src/build-android/jniLibs app/src/main/

소스에서 레이어 바이너리 빌드

앱에 최신 유효성 검사 계층이 필요한 경우 Khronos Group GitHub 저장소에서 최신 소스를 가져온 후 빌드 안내를 따릅니다.

레이어 빌드 확인

NDK의 미리 빌드된 계층을 사용하여 빌드하는지 아니면 최신 소스 코드에서 빌드하는지에 상관없이 빌드 프로세스는 다음과 같은 최종 파일 구조를 생성합니다.

src/main/jniLibs/
  arm64-v8a/
    libVkLayer_khronos_validation.so
  armeabi-v7a/
    libVkLayer_khronos_validation.so

다음 예에서는 예상대로 APK에 유효성 검사 계층이 포함되어 있는지 확인하는 방법을 보여줍니다.

$ jar -xvf project.apk
 ...
 inflated: lib/arm64-v8a/libVkLayer_khronos_validation.so
 ...

레이어 사용 설정

Vulkan API를 사용하면 앱에서 레이어를 사용 설정할 수 있습니다. 계층은 인스턴스를 만드는 중에 사용 설정됩니다. 계층이 가로채는 진입점에는 다음 객체 중 하나가 첫 번째 매개변수로 있어야 합니다.

  • VkInstance
  • VkPhysicalDevice
  • VkDevice
  • VkCommandBuffer
  • VkQueue

vkEnumerateInstanceLayerProperties()를 호출하여 사용 가능한 계층과 그 속성을 나열할 수 있습니다. 시스템은 vkCreateInstance()가 실행될 때 레이어를 사용 설정합니다.

다음 코드 스니펫은 앱이 Vulkan API를 사용하여 프로그래매틱 방식으로 계층을 사용 설정하고 쿼리하는 방법을 보여줍니다.

// Get layer count using null pointer as last parameter
uint32_t instance_layer_present_count = 0;
vkEnumerateInstanceLayerProperties(&instance_layer_present_count, nullptr);

// Enumerate layers with valid pointer in last parameter
VkLayerProperties* layer_props =
    (VkLayerProperties*)malloc(instance_layer_present_count * sizeof(VkLayerProperties));
vkEnumerateInstanceLayerProperties(&instance_layer_present_count, layer_props));

// Make sure the desired validation layer is available
const char *instance_layers[] = {
    "VK_LAYER_KHRONOS_validation"
};

uint32_t instance_layer_request_count =
    sizeof(instance_layers) / sizeof(instance_layers[0]);
for (uint32_t i = 0; i < instance_layer_request_count; i++) {
    bool found = false;
    for (uint32_t j = 0; j < instance_layer_present_count; j++) {
        if (strcmp(instance_layers[i], layer_props[j].layerName) == 0) {
            found = true;
        }
    }
    if (!found) {
        error();
    }
}

// Pass desired layer into vkCreateInstance
VkInstanceCreateInfo instance_info = {};
instance_info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
instance_info.enabledLayerCount = instance_layer_request_count;
instance_info.ppEnabledLayerNames = instance_layers;
...

기본 logcat 출력

유효성 검사 계층은 VALIDATION 태그로 라벨이 지정된 logcat에서 경고 및 오류 메시지를 내보냅니다. 유효성 검사 계층 메시지는 다음과 같습니다.
VALIDATION: UNASSIGNED-CoreValidation-DrawState-QueueForwardProgress(ERROR / SPEC):
            msgNum: 0 - VkQueue 0x7714c92dc0[] is waiting on VkSemaphore 0x192e[]
            that has no way to be signaled.
VALIDATION:     Objects: 1
VALIDATION:         [0] 0x192e, type: 5, name: NULL

디버그 콜백 사용 설정

Debug Utils 확장 프로그램 VK_EXT_debug_utils를 사용하면 애플리케이션에서 제공되는 콜백에 유효성 검사 계층 메시지를 전달하는 디버그 메신저를 만들 수 있습니다. VK_EXT_debug_utils를 사용할 수 없을 때 비슷한 기능을 제공하는 지원 중단된 확장 프로그램 VK_EXT_debug_report도 있습니다.

Debug Utils 확장 프로그램을 사용하기에 앞서 먼저 플랫폼이 이를 지원하는지 확인해야 합니다. 다음 예에서는 디버그 확장 프로그램이 지원되는지 확인하고 확장 프로그램이 지원되는 경우 콜백을 등록하는 방법을 보여줍니다.

// Get the instance extension count
uint32_t inst_ext_count = 0;
vkEnumerateInstanceExtensionProperties(nullptr, &inst_ext_count, nullptr);

// Enumerate the instance extensions
VkExtensionProperties* inst_exts =
    (VkExtensionProperties *)malloc(inst_ext_count * sizeof(VkExtensionProperties));
vkEnumerateInstanceExtensionProperties(nullptr, &inst_ext_count, inst_exts);

const char * enabled_inst_exts[16] = {};
uint32_t enabled_inst_ext_count = 0;

// Make sure the debug utils extension is available
for (uint32_t i = 0; i < inst_ext_count; i++) {
    if (strcmp(inst_exts[i].extensionName,
    VK_EXT_DEBUG_UTILS_EXTENSION_NAME) == 0) {
        enabled_inst_exts[enabled_inst_ext_count++] =
            VK_EXT_DEBUG_UTILS_EXTENSION_NAME;
    }
}

if (enabled_inst_ext_count == 0)
    return;

// Pass the instance extensions into vkCreateInstance
VkInstanceCreateInfo instance_info = {};
instance_info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
instance_info.enabledExtensionCount = enabled_inst_ext_count;
instance_info.ppEnabledExtensionNames = enabled_inst_exts;

PFN_vkCreateDebugUtilsMessengerEXT pfnCreateDebugUtilsMessengerEXT;
PFN_vkDestroyDebugUtilsMessengerEXT pfnDestroyDebugUtilsMessengerEXT;

pfnCreateDebugUtilsMessengerEXT = (PFN_vkCreateDebugUtilsMessengerEXT)
     vkGetDeviceProcAddr(device, "vkCreateDebugUtilsMessengerEXT");
pfnDestroyDebugUtilsMessengerEXT = (PFN_vkDestroyDebugUtilsMessengerEXT)
     vkGetDeviceProcAddr(device, "vkDestroyDebugUtilsMessengerEXT");

assert(pfnCreateDebugUtilsMessengerEXT);
assert(pfnDestroyDebugUtilsMessengerEXT);

// Create the debug messenger callback with desired settings
VkDebugUtilsMessengerEXT debugUtilsMessenger;
if (pfnCreateDebugUtilsMessengerEXT) {
    VkDebugUtilsMessengerCreateInfoEXT messengerInfo;
    constexpr VkDebugUtilsMessageSeverityFlagsEXT kSeveritiesToLog =
            VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT |
            VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT;

    constexpr VkDebugUtilsMessageTypeFlagsEXT kMessagesToLog =
            VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT |
            VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT |
            VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT;

    messengerInfo.sType           = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT;
    messengerInfo.pNext           = nullptr;
    messengerInfo.flags           = 0;
    messengerInfo.messageSeverity = kSeveritiesToLog;
    messengerInfo.messageType     = kMessagesToLog;
    messengerInfo.pfnUserCallback = &DebugUtilsMessenger; // Callback example below
    messengerInfo.pUserData       = nullptr; // Custom user data passed to callback

    pfnCreateDebugUtilsMessengerEXT(instance, &messengerInfo, nullptr, &debugUtilsMessenger);
}

// Later, when shutting down Vulkan, call the following
if (pfnDestroyDebugUtilsMessengerEXT) {
    pfnDestroyDebugUtilsMessengerEXT(instance, debugUtilsMessenger, nullptr);
}

앱이 디버그 콜백을 등록하고 사용 설정하면 시스템은 등록한 콜백으로 디버깅 메시지를 라우팅합니다. 아래에 이러한 콜백의 예가 표시되어 있습니다.

#include <android/log.h>

VKAPI_ATTR VkBool32 VKAPI_CALL DebugUtilsMessenger(
                        VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity,
                        VkDebugUtilsMessageTypeFlagsEXT messageTypes,
                        const VkDebugUtilsMessengerCallbackDataEXT *callbackData,
                        void *userData)
{
   const char validation[]  = "Validation";
   const char performance[] = "Performance";
   const char error[]       = "ERROR";
   const char warning[]     = "WARNING";
   const char unknownType[] = "UNKNOWN_TYPE";
   const char unknownSeverity[] = "UNKNOWN_SEVERITY";
   const char* typeString      = unknownType;
   const char* severityString  = unknownSeverity;
   const char* messageIdName   = callbackData->pMessageIdName;
   int32_t messageIdNumber     = callbackData->messageIdNumber;
   const char* message         = callbackData->pMessage;
   android_LogPriority priority = ANDROID_LOG_UNKNOWN;

   if (messageSeverity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT) {
       severityString = error;
       priority = ANDROID_LOG_ERROR;
   }
   else if (messageSeverity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) {
       severityString = warning;
       priority = ANDROID_LOG_WARN;
   }
   if (messageTypes & VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT) {
       typeString = validation;
   }
   else if (messageTypes & VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT) {
       typeString = performance;
   }

   __android_log_print(priority,
                       "AppName",
                       "%s %s: [%s] Code %i : %s",
                       typeString,
                       severityString,
                       messageIdName,
                       messageIdNumber,
                       message);

   // Returning false tells the layer not to stop when the event occurs, so
   // they see the same behavior with and without validation layers enabled.
   return VK_FALSE;
}

ADB를 사용하여 테스트 기기로 레이어 푸시

테스트 기기로 레이어를 푸시하려면 이 섹션의 단계를 따르세요.

디버깅 사용 설정하기

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

  • 타겟 앱의 매니페스트 파일에 다음 meta-data 요소가 포함됩니다(Android 11(API 수준 'R') 이상을 타겟팅하는 앱에만 적용됨).
    <meta-data android:name="com.android.graphics.injectLayers.enable" android:value="true" /> 이 옵션을 사용하여 애플리케이션을 프로파일링해야 합니다.
  • 타겟 앱이 디버그 가능합니다. 이 옵션은 추가 디버그 정보를 제공하지만 앱 성능에 부정적인 영향을 줄 수 있습니다.
  • 타겟 앱이 루트 액세스 권한을 부여하는 운영체제의 userdebug 빌드에서 실행됩니다.

레이어 로드

Android 9(API 수준 28) 이상을 실행하는 기기라면 Vulkan이 앱의 로컬 저장소에서 계층을 로드할 수 있습니다. Android 10(API 수준 29)에서는 별도의 APK에서 계층 로드를 지원합니다.

기기의 로컬 저장소에 있는 레이어 바이너리

Vulkan은 기기의 임시 데이터 저장소 디렉터리에서 바이너리를 검색하므로 먼저 다음과 같이 Android 디버그 브리지(ADB)를 사용하여 디렉터리로 바이너리를 푸시해야 합니다.

  1. adb push 명령어를 사용하여 원하는 계층 바이너리를 기기의 앱 데이터 저장소에 로드합니다. 다음 예는 libVkLayer_khronos_validation.so를 기기의 /data/local/tmp 디렉터리로 푸시합니다.
    $ adb push libVkLayer_khronos_validation.so /data/local/tmp
    
  2. adb shellrun-as 명령어를 사용하여 앱 프로세스를 통해 계층을 로드합니다. 즉, 이들 바이너리는 루트 액세스 권한을 요청하지 않고도 앱과 동일한 기기 액세스 권한이 있습니다.
    $ adb shell run-as com.example.myapp cp /data/local/tmp/libVkLayer_khronos_validation.so .
    $ adb shell run-as com.example.myapp ls libVkLayer_khronos_validation.so
    
  3. 레이어를 사용 설정합니다.

레이어가 포함된 APK

adb를 사용하여 APK를 설치한 다음 계층을 사용 설정할 수 있습니다.

adb install --abi abi path_to_apk

애플리케이션 외부에서 레이어 사용 설정

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

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

# 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)
adb shell settings put global gpu_debug_layers <layer1:layer2:layerN>

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

설정이 사용 설정되었는지 확인하려면 다음 명령어를 사용하면 됩니다.

$ adb shell settings list global | grep gpu
enable_gpu_debug_layers=1
gpu_debug_app=com.example.myapp
gpu_debug_layers=VK_LAYER_KHRONOS_validation

설정은 기기를 재부팅해도 계속 유지되므로 계층을 로드한 후 설정을 지울 수 있습니다.

$ adb shell settings delete global enable_gpu_debug_layers
$ adb shell settings delete global gpu_debug_app
$ adb shell settings delete global gpu_debug_layers
$ adb shell settings delete global gpu_debug_layer_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

# 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.vulkan.layers <layer1:layer2:layerN>