Warstwy walidacji interfejsu Vulkan na Androidzie

Większość interfejsów API grafiki jawnej nie sprawdza błędów, ponieważ może to obniżyć wydajność. Vulkan ma warstwy walidacji, które sprawdzają błędy podczas programowania, co pozwala uniknąć obniżenia wydajności w kompilacji do publikacji Twojej aplikacji. Warstwy walidacji korzystają z ogólnego mechanizmu warstwowego, który przechwytuje punkty wejścia interfejsu API.

Pojedyncza warstwa walidacji Khronos

Wcześniej Vulkan udostępniał kilka warstw walidacji, które trzeba było włączyć w określonej kolejności. Od wersji 1.1.106.0 pakietu Vulkan SDK aplikacja musi włączyć tylko jedną warstwę walidacji, VK_LAYER_KHRONOS_validation, aby korzystać ze wszystkich funkcji poprzednich warstw walidacji.

Używanie warstw walidacji spakowanych w pliku APK

Spakowanie warstw walidacji w pliku APK zapewnia optymalną zgodność. Warstwy walidacji są dostępne jako wstępnie skompilowane pliki binarne lub można je skompilować z kodu źródłowego.

Używanie wstępnie skompilowanych plików binarnych

Pobierz najnowsze pliki binarne warstwy walidacji Androida Vulkan ze strony wersji w GitHub GitHub.

Najprostszym sposobem na dodanie warstw do pliku APK jest wyodrębnienie wstępnie skompilowanych plików binarnych warstwy do katalogu src/main/jniLibs/ modułu z zachowaniem katalogów ABI (takich jak arm64-v8a czy x86-64), np. tak:

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

Kompilowanie warstwy walidacji z kodu źródłowego

Aby debugować kod źródłowy warstwy walidacji, pobierz najnowszy kod źródłowy z repozytorium Khronos Group GitHub i postępuj zgodnie z instrukcjami kompilacji.

Sprawdzanie, czy warstwa walidacji jest prawidłowo spakowana

Niezależnie od tego, czy kompilujesz za pomocą wstępnie skompilowanych warstw Khronos, czy warstw skompilowanych ze źródła, proces kompilacji tworzy w pliku APK strukturę plików podobną do tej:

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

To polecenie pokazuje, jak sprawdzić, czy plik APK zawiera warstwę walidacji zgodnie z oczekiwaniami:

$ 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

Włączanie warstwy walidacji podczas tworzenia instancji

Interfejs Vulkan API umożliwia aplikacji włączanie warstw podczas tworzenia instancji. Punkty wejścia przechwytywane przez warstwę muszą mieć jako pierwszy parametr jeden z tych obiektów:

  • VkInstance
  • VkPhysicalDevice
  • VkDevice
  • VkCommandBuffer
  • VkQueue

Aby wyświetlić listę dostępnych warstw i ich właściwości, wywołaj funkcję vkEnumerateInstanceLayerProperties(). Vulkan włącza warstwy, gdy vkCreateInstance() wykonywana jest funkcja.

Ten fragment kodu pokazuje, jak aplikacja może używać interfejsu Vulkan API do programowego wysyłania zapytań o warstwy i ich włączania:

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

Domyślne dane wyjściowe logcat

Warstwa walidacji emituje w Logcat ostrzeżenia i komunikaty o błędach oznaczone tagiem VALIDATION. Komunikat warstwy walidacji wygląda tak (podziały wierszy zostały dodane, aby ułatwić przewijanie):

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)

Włączanie wywołania zwrotnego debugowania

Rozszerzenie Debug Utils VK_EXT_debug_utils umożliwia aplikacji utworzenie narzędzia do debugowania, które przekazuje komunikaty warstwy walidacji do wywołania zwrotnego dostarczonego przez aplikację. Twoje urządzenie może nie obsługiwać tego rozszerzenia, ale jest ono zaimplementowane w najnowszych warstwach walidacji. Istnieje też wycofane rozszerzenie VK_EXT_debug_report, które zapewnia podobne możliwości, jeśli rozszerzenie VK_EXT_debug_utils jest niedostępne.

Zanim zaczniesz korzystać z rozszerzenia Debug Utils, sprawdź, czy jest ono obsługiwane przez urządzenie lub wczytaną warstwę walidacji. Ten przykład pokazuje, jak sprawdzić, czy rozszerzenie Debug Utils jest obsługiwane, i zarejestrować wywołanie zwrotne, jeśli rozszerzenie jest obsługiwane przez urządzenie lub warstwę walidacji.

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

Gdy aplikacja zarejestruje i włączy wywołanie zwrotne, system będzie kierować do niego komunikaty debugowania.

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

Używanie zewnętrznych warstw walidacji

Nie musisz pakować warstw walidacji w pliku APK. Urządzenia z Androidem 9 (poziom 28 interfejsu API) i nowszym mogą używać warstw walidacji zewnętrznych w stosunku do pliku binarnego i dynamicznie je włączać i wyłączać. Aby przesłać warstwy walidacji na urządzenie testowe, wykonaj czynności opisane w tej sekcji:

Włączanie w aplikacji możliwości używania zewnętrznych warstw walidacji

Model zabezpieczeń i zasady Androida znacznie różnią się od innych platform. Aby wczytać zewnętrzne warstwy walidacji, musi być spełniony jeden z tych warunków:

  • Aplikacja docelowa ma możliwość debugowania. Ta opcja zapewnia więcej informacji o debugowaniu, ale może negatywnie wpłynąć na wydajność aplikacji.

  • Aplikacja docelowa jest uruchamiana w wersji userdebug systemu operacyjnego, która przyznaje dostęp do roota.

  • Tylko w przypadku aplikacji kierowanych na Androida 11 (poziom 30 interfejsu API) lub nowszy: docelowy plik manifestu Androida zawiera ten element meta-data:

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

Wczytywanie zewnętrznej warstwy walidacji

Urządzenia z Androidem 9 (poziom 28 interfejsu API) i nowszym umożliwiają Vulkanowi wczytywanie warstwy walidacji z pamięci lokalnej aplikacji. Od Androida 10 (poziom 29 interfejsu API) Vulkan może też wczytywać warstwę walidacji z osobnego pliku APK. Możesz wybrać dowolną metodę, o ile jest ona obsługiwana przez Twoją wersję Androida.

Wczytywanie pliku binarnego warstwy walidacji z pamięci lokalnej urządzenia

Ponieważ Vulkan szuka pliku binarnego w katalogu tymczasowych danych urządzenia, musisz najpierw przesłać plik binarny do tego katalogu za pomocą narzędzia Android Debug Bridge (adb), wykonując te czynności:

  1. Użyj polecenia adb push, aby wczytać plik binarny warstwy do pamięci danych aplikacji na urządzeniu:

    $ adb push libVkLayer_khronos_validation.so /data/local/tmp
    
  2. Użyj poleceń adb shell i run-as , aby wczytać warstwę za pomocą procesu aplikacji. Oznacza to, że plik binarny ma taki sam dostęp do urządzenia jak aplikacja, bez konieczności uzyskiwania dostępu do roota.

    $ 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. Włącz warstwę.

Wczytywanie pliku binarnego warstwy walidacji z innego pliku APK

Możesz użyć narzędzia adb, aby zainstalować plik APK zawierający warstwę, a następnie włączyć warstwę.

adb install --abi abi path_to_apk

Włączanie warstw poza aplikacją

Warstwy Vulkan możesz włączać na poziomie aplikacji lub globalnie. Ustawienia na poziomie aplikacji są zachowywane po ponownym uruchomieniu, a właściwości globalne są czyszczone po ponownym uruchomieniu.

Włączanie warstw na poziomie aplikacji

Aby włączyć warstwy na poziomie aplikacji, wykonaj te czynności:

  1. Użyj polecenia adb shell settings, aby włączyć warstwy:

    $ adb shell settings put global enable_gpu_debug_layers 1
    
  2. Określ aplikację docelową, w której mają być włączone warstwy:

    $ adb shell settings put global gpu_debug_app <package_name>
    
  3. Określ listę warstw do włączenia (od góry do dołu), oddzielając każdą warstwę dwukropkiem:

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

    Ponieważ mamy jedną warstwę walidacji Khronos, polecenie będzie prawdopodobnie wyglądać tak:

    $ adb shell settings put global gpu_debug_layers VK_LAYER_KHRONOS_validation
    
  4. Określ co najmniej 1 pakiet, w którym mają być wyszukiwane warstwy:

    $ adb shell settings put global
      gpu_debug_layer_app <package1:package2:packageN>
    

Aby sprawdzić, czy ustawienia są włączone, użyj tych poleceń:

$ 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

Ponieważ zastosowane ustawienia są zachowywane po ponownym uruchomieniu urządzenia, po wczytaniu warstw możesz wyczyścić ustawienia:

$ 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

Włączanie warstw globalnie

Możesz włączyć globalnie jedną lub kilka warstw do następnego ponownego uruchomienia. Spowoduje to próbę wczytania warstw we wszystkich aplikacjach, w tym w natywnych plikach wykonywalnych.

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