Android의 Vulkan 유효성 검사 계층

대부분의 명시적 그래픽 API는 오류 검사를 하면 성능이 저하될 수 있으므로 오류 검사를 하지 않습니다. Vulkan에는 개발 중 오류 검사를 제공하는 유효성 검사 계층이 있어 앱 출시 빌드에서 성능 저하를 방지합니다. 유효성 검사 계층은 API 진입점을 가로채는 범용 계층 메커니즘에 의존합니다.

단일 Khronos 유효성 검사 계층

이전에 Vulkan에서는 특정 순서로 사용 설정해야 하는 여러 유효성 검사 계층을 제공했습니다. 1.1.106.0 Vulkan SDK 버전부터는 이전 유효성 검사 계층의 모든 기능을 가져오려면 앱에서 단일 유효성 검사 계층VK_LAYER_KHRONOS_validation만 사용 설정하면 됩니다.

APK에 패키징된 유효성 검사 계층 사용

APK 내에서 유효성 검사 계층을 패키징하면 최적의 호환성이 보장됩니다. 유효성 검사 계층은 사전 빌드된 바이너리로 사용 가능하거나 소스 코드에서 빌드 가능합니다.

사전 빌드된 바이너리 사용

GitHub 출시 페이지에서 최신 Android Vulkan 유효성 검사 계층 바이너리를 다운로드하세요.

APK에 계층을 추가하는 가장 쉬운 방법은 다음과 같이 ABI 디렉터리(예: arm64-v8a 또는 x86-64)를 그대로 유지한 채 사전 빌드된 계층 바이너리를 모듈의 src/main/jniLibs/ 디렉터리에 추출하는 것입니다.

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

소스 코드에서 유효성 검사 계층 빌드

유효성 검사 계층 소스 코드로 디버깅하려면 Khronos Group GitHub 저장소에서 최신 소스를 가져온 후 빌드 안내를 따릅니다.

유효성 검사 계층이 정확하게 패키징되었는지 확인

빌드 시 사용하는 계층이 Khronos의 사전 빌드된 계층이든 소스에서 빌드된 계층이든 상관없이 빌드 프로세스는 다음과 같이 APK에서 최종 파일 구조를 생성합니다.

lib/
  arm64-v8a/
    libVkLayer_khronos_validation.so
  armeabi-v7a/
    libVkLayer_khronos_validation.so
  x86/
    libVkLayer_khronos_validation.so
  x86-64/
    libVkLayer_khronos_validation.so

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

$ jar -tf project.apk | grep libVkLayer
lib/x86_64/libVkLayer_khronos_validation.so
lib/armeabi-v7a/libVkLayer_khronos_validation.so
lib/arm64-v8a/libVkLayer_khronos_validation.so
lib/x86/libVkLayer_khronos_validation.so

인스턴스 생성 중 유효성 검사 계층 사용 설정

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

  • VkInstance
  • VkPhysicalDevice
  • VkDevice
  • VkCommandBuffer
  • VkQueue

vkEnumerateInstanceLayerProperties()를 호출하여 사용 가능한 계층과 관련 속성을 나열합니다. Vulkan은 vkCreateInstance()가 실행될 때 계층을 사용 설정합니다.

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

// Enable just the Khronos validation layer.
static const char *layers[] = {"VK_LAYER_KHRONOS_validation"};

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

// Enumerate layers with a valid pointer in the last parameter.
VkLayerProperties layer_props[instance_layer_present_count];
vkEnumerateInstanceLayerProperties(&instance_layer_present_count, layer_props);

// Make sure selected validation layers are available.
VkLayerProperties *layer_props_end = layer_props + instance_layer_present_count;
for (const char* layer:layers) {
  assert(layer_props_end !=
  std::find_if(layer_props, layer_props_end, [layer](VkLayerProperties layerProperties) {
    return strcmp(layerProperties.layerName, layer) == 0;
  }));
}

// Create a Vulkan instance, requesting all enabled layers or extensions
// available on the system
VkInstanceCreateInfo instanceCreateInfo{
  .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
  .pNext = nullptr,
  .pApplicationInfo = &appInfo,
  .enabledLayerCount = sizeof(layers) / sizeof(layers[0]),
  .ppEnabledLayerNames = layers,

기본 logcat 출력

유효성 검사 계층은 VALIDATION 태그로 라벨이 지정된 logcat에서 경고 및 오류 메시지를 내보냅니다. 유효성 검사 계층 메시지는 다음과 같습니다(여기에는 좀 더 쉽게 스크롤할 수 있도록 줄 바꿈이 추가됨).

Validation -- Validation Error:
  [ VUID-VkDeviceQueueCreateInfo-pQueuePriorities-parameter ]
Object 0: VK_NULL_HANDLE, type = VK_OBJECT_TYPE_DEVICE; | MessageID = 0xd6d720c6 |
vkCreateDevice: required parameter
  pCreateInfo->pQueueCreateInfos[0].pQueuePriorities specified as NULL.
The Vulkan spec states: pQueuePriorities must be a valid pointer to an array of
  queueCount float values
  (https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html
  #VUID-VkDeviceQueueCreateInfo-pQueuePriorities-parameter)

디버그 콜백 사용 설정

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

Debug Utils 확장 프로그램을 사용하기 전에 기기 또는 로드된 유효성 검사 계층에서 이 확장 프로그램을 지원하는지 확인해야 합니다. 다음 예에서는 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[inst_ext_count];
vkEnumerateInstanceExtensionProperties(nullptr, &inst_ext_count, inst_exts);

// Check for debug utils extension within the system driver or loader.
// Check if the debug utils extension is available (in the driver).
VkExtensionProperties *inst_exts_end = inst_exts + inst_ext_count;
bool debugUtilsExtAvailable = inst_exts_end !=
  std::find_if(inst_exts, inst_exts_end, [](VkExtensionProperties
    extensionProperties) {
    return strcmp(extensionProperties.extensionName,
      VK_EXT_DEBUG_UTILS_EXTENSION_NAME) == 0;
  });

if ( !debugUtilsExtAvailable ) {
  // Also check the layers for the debug utils extension.
  for (auto layer: layer_props) {
    uint32_t layer_ext_count;
    vkEnumerateInstanceExtensionProperties(layer.layerName, &layer_ext_count,
      nullptr);
    if (layer_ext_count == 0) continue;
    VkExtensionProperties layer_exts[layer_ext_count];
    vkEnumerateInstanceExtensionProperties(layer.layerName, &layer_ext_count,
    layer_exts);

    VkExtensionProperties * layer_exts_end = layer_exts + layer_ext_count;
    debugUtilsExtAvailable = layer_exts != std::find_if(
      layer_exts, layer_exts_end,[](VkExtensionProperties extensionProperties) {
        return strcmp(extensionProperties.extensionName,
        VK_EXT_DEBUG_UTILS_EXTENSION_NAME) == 0;
      });
    if (debugUtilsExtAvailable) {
        // Add the including layer into the layer request list if necessary.
        break;
    }
  }
}

if (!debugUtilsExtAvailable) return; // since this snippet depends on debugUtils

const char * enabled_inst_exts[] = { ..., VK_EXT_DEBUG_UTILS_EXTENSION_NAME };
uint32_t enabled_extension_count =
  sizeof(enabled_inst_exts)/sizeof(enabled_inst_exts[0]);

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

// NOTE: Can still return VK_ERROR_EXTENSION_NOT_PRESENT if validation layer
// isn't loaded.
vkCreateInstance(&instance_info, nullptr, &instance);

auto pfnCreateDebugUtilsMessengerEXT =
  (PFN_vkCreateDebugUtilsMessengerEXT)vkGetInstanceProcAddr(
    tutorialInstance, "vkCreateDebugUtilsMessengerEXT");
auto pfnDestroyDebugUtilsMessengerEXT =
  (PFN_vkDestroyDebugUtilsMessengerEXT)vkGetInstanceProcAddr(
    tutorialInstance, "vkDestroyDebugUtilsMessengerEXT");

// Create the debug messenger callback with your the settings you want.
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;

  // The DebugUtilsMessenger callback is explained in the following section.
  messengerInfo.pfnUserCallback = &DebugUtilsMessenger;
  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;
}

외부 유효성 검사 계층 사용

APK에서는 유효성 검사 계층을 패키징할 필요가 없습니다. Android 9(API 수준 28) 이상을 실행하는 기기에서는 바이너리 외부의 유효성 검사 계층을 사용하고 이를 동적으로 끄거나 켤 수 있습니다. 테스트 기기로 유효성 검사 계층을 푸시하려면 이 섹션의 단계를 따르세요.

앱에서 외부 유효성 검사 계층을 사용하도록 설정

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

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

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

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

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

외부 유효성 검사 계층 로드

Android 9(API 수준 28) 이상을 실행하는 기기에서는 Vulkan을 통해 앱의 로컬 저장소에서 유효성 검사 계층을 로드할 수 있습니다. Android 10(API 수준 29)부터 Vulkan에서는 별도의 APK에서도 유효성 검사 계층을 로드합니다. Android 버전에서 지원하는 한 개발자는 원하는 모든 메서드를 선택할 수 있습니다.

기기의 로컬 저장소에서 유효성 검사 계층 바이너리 로드

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

  1. adb push 명령어를 사용하여 계층 바이너리를 기기의 앱 데이터 저장소에 로드합니다.

    $ 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

애플리케이션 외부에서 계층 사용 설정

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

앱별로 계층 사용 설정

다음 단계에서는 앱별로 계층을 사용 설정하는 방법을 설명합니다.

  1. adb 셸 설정을 사용하여 계층을 사용 설정합니다.

    $ adb shell settings put global enable_gpu_debug_layers 1
    
  2. 계층을 사용 설정할 타겟 애플리케이션을 지정합니다.

    $ adb shell settings put global gpu_debug_app <package_name>
    
  3. 사용 설정할 계층 목록을 지정합니다(하향식). 이때 각 계층을 콜론으로 구분합니다.

    $ adb shell settings put global gpu_debug_layers <layer1:layer2:layerN>
    

    단일 Khronos 유효성 검사 계층이 있으므로 명령어는 다음과 같습니다.

    $ adb shell settings put global gpu_debug_layers VK_LAYER_KHRONOS_validation
    
  4. 다음 범위 내의 계층을 검색할 패키지를 하나 이상 지정합니다.

    $ 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

전역적으로 계층 사용 설정

다음번 재부팅까지 하나 이상의 계층을 전역적으로 사용 설정할 수 있습니다. 이 경우 네이티브 실행 파일을 포함하여 모든 애플리케이션의 계층을 로드하려는 시도가 발생합니다.

$ adb shell setprop debug.vulkan.layers <layer1:layer2:layerN>