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;
    ...
    

디버그 콜백 사용 설정

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